From 3c420310865071185e192269bbeae2f70e1c04cf Mon Sep 17 00:00:00 2001 From: dezhidki Date: Tue, 4 Jul 2023 12:23:23 +0300 Subject: [PATCH 01/34] Upgrade to Python 3.11; bump backend packages --- poetry.lock | 2380 +++++++++++++++++++--------------- pyproject.toml | 68 +- timApp/Dockerfile | 22 +- timApp/modules/cs/Dockerfile | 10 +- 4 files changed, 1383 insertions(+), 1097 deletions(-) diff --git a/poetry.lock b/poetry.lock index c212700aba..6140fb98f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,131 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.8.4" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"}, + {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"}, + {file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5"}, + {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949"}, + {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea"}, + {file = "aiohttp-3.8.4-cp310-cp310-win32.whl", hash = "sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1"}, + {file = "aiohttp-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4"}, + {file = "aiohttp-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05"}, + {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b"}, + {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24"}, + {file = "aiohttp-3.8.4-cp311-cp311-win32.whl", hash = "sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d"}, + {file = "aiohttp-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc"}, + {file = "aiohttp-3.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01"}, + {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71"}, + {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff"}, + {file = "aiohttp-3.8.4-cp36-cp36m-win32.whl", hash = "sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777"}, + {file = "aiohttp-3.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e"}, + {file = "aiohttp-3.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab"}, + {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6"}, + {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241"}, + {file = "aiohttp-3.8.4-cp37-cp37m-win32.whl", hash = "sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a"}, + {file = "aiohttp-3.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15"}, + {file = "aiohttp-3.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8"}, + {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275"}, + {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d"}, + {file = "aiohttp-3.8.4-cp38-cp38-win32.whl", hash = "sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54"}, + {file = "aiohttp-3.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567"}, + {file = "aiohttp-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2"}, + {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14"}, + {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4"}, + {file = "aiohttp-3.8.4-cp39-cp39-win32.whl", hash = "sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a"}, + {file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"}, + {file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<4.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "cchardet"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" [[package]] name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -16,7 +137,6 @@ files = [ name = "alembic" version = "1.11.1" description = "A database migration tool for SQLAlchemy." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -34,24 +154,22 @@ tz = ["python-dateutil"] [[package]] name = "amqp" -version = "2.6.1" +version = "5.1.1" description = "Low-level AMQP client for Python (fork of amqplib)." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" files = [ - {file = "amqp-2.6.1-py2.py3-none-any.whl", hash = "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"}, - {file = "amqp-2.6.1.tar.gz", hash = "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21"}, + {file = "amqp-5.1.1-py3-none-any.whl", hash = "sha256:6f0956d2c23d8fa6e7691934d8c3930eadb44972cbbd1a7ae3a520f735d43359"}, + {file = "amqp-5.1.1.tar.gz", hash = "sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2"}, ] [package.dependencies] -vine = ">=1.1.3,<5.0.0a1" +vine = ">=5.0.0" [[package]] name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -61,32 +179,31 @@ files = [ [[package]] name = "attrs" -version = "21.4.0" +version = "23.1.0" description = "Classes Without Boilerplate" -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" files = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, ] [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] [[package]] name = "authlib" -version = "1.0.0rc1" +version = "1.2.1" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." -category = "main" optional = false python-versions = "*" files = [ - {file = "Authlib-1.0.0rc1-py2.py3-none-any.whl", hash = "sha256:add1855e7acad8da8b93b3895fe608d4304116e9cfbc6038591a0c483f44cc7b"}, - {file = "Authlib-1.0.0rc1.tar.gz", hash = "sha256:abc68aadc0d305576975d77d8a7f9c6d9e3b5434f23facb424d0e9d545e8b649"}, + {file = "Authlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:c88984ea00149a90e3537c964327da930779afa4564e354edfd98410bea01911"}, + {file = "Authlib-1.2.1.tar.gz", hash = "sha256:421f7c6b468d907ca2d9afede256f068f87e34d23dd221c07d13d4c234726afb"}, ] [package.dependencies] @@ -94,25 +211,22 @@ cryptography = ">=3.2" [[package]] name = "autopep8" -version = "1.7.0" +version = "2.0.2" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "autopep8-1.7.0-py2.py3-none-any.whl", hash = "sha256:6f09e90a2be784317e84dc1add17ebfc7abe3924239957a37e5040e27d812087"}, - {file = "autopep8-1.7.0.tar.gz", hash = "sha256:ca9b1a83e53a7fad65d731dc7a2a2d50aa48f43850407c59f6a1a306c4201142"}, + {file = "autopep8-2.0.2-py2.py3-none-any.whl", hash = "sha256:86e9303b5e5c8160872b2f5ef611161b2893e9bfe8ccc7e2f76385947d57a2f1"}, + {file = "autopep8-2.0.2.tar.gz", hash = "sha256:f9849cdd62108cb739dbcdbfb7fdcc9a30d1b63c4cc3e1c1f893b5360941b61c"}, ] [package.dependencies] -pycodestyle = ">=2.9.1" -toml = "*" +pycodestyle = ">=2.10.0" [[package]] name = "babel" version = "2.12.1" description = "Internationalization utilities" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -122,28 +236,34 @@ files = [ [[package]] name = "bcrypt" -version = "3.2.2" +version = "4.0.1" description = "Modern password hashing for your software and your servers" -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "bcrypt-3.2.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:7180d98a96f00b1050e93f5b0f556e658605dd9f524d0b0e68ae7944673f525e"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:61bae49580dce88095d669226d5076d0b9d927754cedbdf76c6c9f5099ad6f26"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88273d806ab3a50d06bc6a2fc7c87d737dd669b76ad955f449c43095389bc8fb"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6d2cb9d969bfca5bc08e45864137276e4c3d3d7de2b162171def3d188bf9d34a"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b02d6bfc6336d1094276f3f588aa1225a598e27f8e3388f4db9948cb707b521"}, - {file = "bcrypt-3.2.2-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c46100e315c3a5b90fdc53e429c006c5f962529bc27e1dfd656292c20ccc40"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7d9ba2e41e330d2af4af6b1b6ec9e6128e91343d0b4afb9282e54e5508f31baa"}, - {file = "bcrypt-3.2.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cd43303d6b8a165c29ec6756afd169faba9396a9472cdff753fe9f19b96ce2fa"}, - {file = "bcrypt-3.2.2-cp36-abi3-win32.whl", hash = "sha256:4e029cef560967fb0cf4a802bcf4d562d3d6b4b1bf81de5ec1abbe0f1adb027e"}, - {file = "bcrypt-3.2.2-cp36-abi3-win_amd64.whl", hash = "sha256:7ff2069240c6bbe49109fe84ca80508773a904f5a8cb960e02a977f7f519b129"}, - {file = "bcrypt-3.2.2.tar.gz", hash = "sha256:433c410c2177057705da2a9f2cd01dd157493b2a7ac14c8593a16b3dab6b6bfb"}, + {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, + {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, + {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, + {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, ] -[package.dependencies] -cffi = ">=1.1" - [package.extras] tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] @@ -152,7 +272,6 @@ typecheck = ["mypy"] name = "beautifulsoup4" version = "4.12.2" description = "Screen-scraping library" -category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -169,44 +288,56 @@ lxml = ["lxml"] [[package]] name = "billiard" -version = "3.6.4.0" +version = "4.1.0" description = "Python multiprocessing fork with improvements and bugfixes" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"}, - {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"}, + {file = "billiard-4.1.0-py3-none-any.whl", hash = "sha256:0f50d6be051c6b2b75bfbc8bfd85af195c5739c281d3f5b86a5640c65563614a"}, + {file = "billiard-4.1.0.tar.gz", hash = "sha256:1ad2eeae8e28053d729ba3373d34d9d6e210f6e4d8bf0a9c64f92bd053f1edf5"}, ] [[package]] name = "black" -version = "22.12.0" +version = "23.3.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, -] - -[package.dependencies] + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] + +[package.dependencies] +aiohttp = {version = ">=3.7.4", optional = true, markers = "extra == \"d\""} click = ">=8.0.0" mypy-extensions = ">=0.4.3" +packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -216,14 +347,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleach" -version = "5.0.1" +version = "6.0.0" description = "An easy safelist-based HTML-sanitizing tool." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, - {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, + {file = "bleach-6.0.0-py3-none-any.whl", hash = "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4"}, + {file = "bleach-6.0.0.tar.gz", hash = "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414"}, ] [package.dependencies] @@ -232,13 +362,22 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.2)"] -dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] + +[[package]] +name = "blinker" +version = "1.6.2" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.7" +files = [ + {file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"}, + {file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"}, +] [[package]] name = "brotli" version = "1.0.9" description = "Python bindings for the Brotli compression library" -category = "main" optional = false python-versions = "*" files = [ @@ -330,7 +469,6 @@ files = [ name = "cachelib" version = "0.9.0" description = "A collection of cache libraries in the same API interface." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -340,74 +478,75 @@ files = [ [[package]] name = "celery" -version = "4.4.0" +version = "5.3.1" description = "Distributed Task Queue." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*," +python-versions = ">=3.8" files = [ - {file = "celery-4.4.0-py2.py3-none-any.whl", hash = "sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f"}, - {file = "celery-4.4.0.tar.gz", hash = "sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2"}, + {file = "celery-5.3.1-py3-none-any.whl", hash = "sha256:27f8f3f3b58de6e0ab4f174791383bbd7445aff0471a43e99cfd77727940753f"}, + {file = "celery-5.3.1.tar.gz", hash = "sha256:f84d1c21a1520c116c2b7d26593926581191435a03aa74b77c941b93ca1c6210"}, ] [package.dependencies] -billiard = ">=3.6.1,<4.0" -kombu = ">=4.6.7,<4.7" -pytz = ">0.0-dev" -redis = {version = ">=3.2.0", optional = true, markers = "extra == \"redis\""} -vine = "1.3.0" +billiard = ">=4.1.0,<5.0" +click = ">=8.1.2,<9.0" +click-didyoumean = ">=0.3.0" +click-plugins = ">=1.1.1" +click-repl = ">=0.2.0" +kombu = ">=5.3.1,<6.0" +python-dateutil = ">=2.8.2" +redis = {version = ">=4.5.2,<4.5.5 || >4.5.5", optional = true, markers = "extra == \"redis\""} +tzdata = ">=2022.7" +vine = ">=5.0.0,<6.0" [package.extras] -arangodb = ["pyArango (>=1.3.2)"] -auth = ["cryptography"] -azureblockblob = ["azure-common (==1.1.5)", "azure-storage (==0.36.0)", "azure-storage-common (==1.1.0)"] +arangodb = ["pyArango (>=2.0.1)"] +auth = ["cryptography (==41.0.1)"] +azureblockblob = ["azure-storage-blob (>=12.15.0)"] brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] -cassandra = ["cassandra-driver"] -consul = ["python-consul"] -cosmosdbsql = ["pydocumentdb (==2.3.2)"] -couchbase = ["couchbase", "couchbase-cffi"] -couchdb = ["pycouchdb"] -django = ["Django (>=1.11)"] -dynamodb = ["boto3 (>=1.9.178)"] -elasticsearch = ["elasticsearch"] -eventlet = ["eventlet (>=0.24.1)"] -gevent = ["gevent"] -librabbitmq = ["librabbitmq (>=1.5.0)"] -lzma = ["backports.lzma"] -memcache = ["pylibmc"] -mongodb = ["pymongo[srv] (>=3.3.0)"] -msgpack = ["msgpack"] -pymemcache = ["python-memcached"] -pyro = ["pyro4"] -redis = ["redis (>=3.2.0)"] -riak = ["riak (>=2.0)"] -s3 = ["boto3 (>=1.9.125)"] +cassandra = ["cassandra-driver (>=3.25.0,<4)"] +consul = ["python-consul2 (==0.1.5)"] +cosmosdbsql = ["pydocumentdb (==2.3.5)"] +couchbase = ["couchbase (>=3.0.0)"] +couchdb = ["pycouchdb (==1.14.2)"] +django = ["Django (>=2.2.28)"] +dynamodb = ["boto3 (>=1.26.143)"] +elasticsearch = ["elasticsearch (<8.0)"] +eventlet = ["eventlet (>=0.32.0)"] +gevent = ["gevent (>=1.5.0)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +memcache = ["pylibmc (==1.6.3)"] +mongodb = ["pymongo[srv] (>=4.0.2)"] +msgpack = ["msgpack (==1.0.5)"] +pymemcache = ["python-memcached (==1.59)"] +pyro = ["pyro4 (==4.82)"] +pytest = ["pytest-celery (==0.0.0)"] +redis = ["redis (>=4.5.2,!=4.5.5)"] +s3 = ["boto3 (>=1.26.143)"] slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem"] -sqlalchemy = ["sqlalchemy"] -sqs = ["boto3 (>=1.9.125)", "pycurl"] +solar = ["ephem (==4.1.4)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.0)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] -zstd = ["zstandard"] +zstd = ["zstandard (==0.21.0)"] [[package]] name = "certifi" -version = "2023.7.22" +version = "2023.5.7" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, + {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, ] [[package]] name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -482,109 +621,155 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.2.0" +version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, ] [[package]] name = "click" -version = "8.1.6" +version = "8.1.3" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click-didyoumean" +version = "0.3.0" +description = "Enables git-like *did-you-mean* feature in click" +optional = false +python-versions = ">=3.6.2,<4.0.0" +files = [ + {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"}, + {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"}, +] + +[package.dependencies] +click = ">=7" + +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +optional = false +python-versions = "*" +files = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] + +[[package]] +name = "click-repl" +version = "0.3.0" +description = "REPL plugin for Click" +optional = false +python-versions = ">=3.6" +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[package.dependencies] +click = ">=7.0" +prompt-toolkit = ">=3.0.36" + +[package.extras] +testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] + [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -596,7 +781,6 @@ files = [ name = "commonmark" version = "0.9.1" description = "Python parser for the CommonMark Markdown spec" -category = "main" optional = false python-versions = "*" files = [ @@ -609,35 +793,30 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "cryptography" -version = "41.0.3" +version = "41.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, - {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, - {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, - {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, - {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, - {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, - {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, - {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, - {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, - {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, - {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, + {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699"}, + {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b"}, + {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3"}, + {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db"}, + {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31"}, + {file = "cryptography-41.0.1-cp37-abi3-win32.whl", hash = "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5"}, + {file = "cryptography-41.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039"}, + {file = "cryptography-41.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a"}, + {file = "cryptography-41.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5"}, + {file = "cryptography-41.0.1.tar.gz", hash = "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006"}, ] [package.dependencies] @@ -657,7 +836,6 @@ test-randomorder = ["pytest-randomly"] name = "cssselect" version = "1.2.0" description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -669,7 +847,6 @@ files = [ name = "defusedxml" version = "0.7.1" description = "XML bomb protection for Python stdlib modules" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -679,14 +856,13 @@ files = [ [[package]] name = "docformatter" -version = "1.7.5" +version = "1.7.3" description = "Formats docstrings to follow PEP 257" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "docformatter-1.7.5-py3-none-any.whl", hash = "sha256:a24f5545ed1f30af00d106f5d85dc2fce4959295687c24c8f39f5263afaf9186"}, - {file = "docformatter-1.7.5.tar.gz", hash = "sha256:ffed3da0daffa2e77f80ccba4f0e50bfa2755e1c10e130102571c890a61b246e"}, + {file = "docformatter-1.7.3-py3-none-any.whl", hash = "sha256:ba776f6305ff0ae9e1f42178975c0956ce01216e80b9d3e3e43daae9f742e321"}, + {file = "docformatter-1.7.3.tar.gz", hash = "sha256:f6ce59631d4ecc41af2780787b88f5dab94cdf1383796b1110318040c2f4ea36"}, ] [package.dependencies] @@ -700,7 +876,6 @@ tomli = ["tomli (>=2.0.0,<3.0.0)"] name = "docutils" version = "0.19" description = "Docutils -- Python Documentation Utilities" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -710,14 +885,13 @@ files = [ [[package]] name = "elementpath" -version = "4.1.5" +version = "4.1.4" description = "XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and lxml" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "elementpath-4.1.5-py3-none-any.whl", hash = "sha256:2ac1a2fb31eb22bbbf817f8cf6752f844513216263f0e3892c8e79782fe4bb55"}, - {file = "elementpath-4.1.5.tar.gz", hash = "sha256:c2d6dc524b29ef751ecfc416b0627668119d8812441c555d7471da41d4bacb8d"}, + {file = "elementpath-4.1.4-py3-none-any.whl", hash = "sha256:e7c6d25546dfb381a2c9cde3b78c0c40f52811e06eb810faf019e16c531a74bf"}, + {file = "elementpath-4.1.4.tar.gz", hash = "sha256:f991c42ff66fa06e219141ccf65890261e6635b448e7d4c2d8b62dc5bf1de9e8"}, ] [package.extras] @@ -727,7 +901,6 @@ dev = ["Sphinx", "coverage", "flake8", "lxml", "lxml-stubs", "memory-profiler", name = "exceptiongroup" version = "1.1.2" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -742,7 +915,6 @@ test = ["pytest (>=6)"] name = "filelock" version = "3.12.2" description = "A platform independent file lock." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -756,21 +928,21 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p [[package]] name = "flask" -version = "2.1.3" +version = "2.3.2" description = "A simple framework for building complex web applications." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Flask-2.1.3-py3-none-any.whl", hash = "sha256:9013281a7402ad527f8fd56375164f3aa021ecfaff89bfe3825346c24f87e04c"}, - {file = "Flask-2.1.3.tar.gz", hash = "sha256:15972e5017df0575c3d6c090ba168b6db90259e620ac8d7ea813a396bad5b6cb"}, + {file = "Flask-2.3.2-py3-none-any.whl", hash = "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0"}, + {file = "Flask-2.3.2.tar.gz", hash = "sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef"}, ] [package.dependencies] -click = ">=8.0" -itsdangerous = ">=2.0" -Jinja2 = ">=3.0" -Werkzeug = ">=2.0" +blinker = ">=1.6.2" +click = ">=8.1.3" +itsdangerous = ">=2.1.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=2.3.3" [package.extras] async = ["asgiref (>=3.2)"] @@ -780,7 +952,6 @@ dotenv = ["python-dotenv"] name = "flask-assets" version = "2.0" description = "Asset management for Flask, to compress and merge CSS and Javascript files." -category = "main" optional = false python-versions = "*" files = [ @@ -796,7 +967,6 @@ webassets = ">=2.0" name = "flask-caching" version = "2.0.2" description = "Adds caching support to Flask applications." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -812,7 +982,6 @@ Flask = "<3" name = "flask-compress" version = "1.13" description = "Compress responses in your Flask app with gzip, deflate or brotli." -category = "main" optional = false python-versions = "*" files = [ @@ -826,18 +995,17 @@ flask = "*" [[package]] name = "flask-migrate" -version = "3.1.0" +version = "4.0.4" description = "SQLAlchemy database migrations for Flask applications using Alembic." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "Flask-Migrate-3.1.0.tar.gz", hash = "sha256:57d6060839e3a7f150eaab6fe4e726d9e3e7cffe2150fb223d73f92421c6d1d9"}, - {file = "Flask_Migrate-3.1.0-py3-none-any.whl", hash = "sha256:a6498706241aba6be7a251078de9cf166d74307bca41a4ca3e403c9d39e2f897"}, + {file = "Flask-Migrate-4.0.4.tar.gz", hash = "sha256:73293d40b10ac17736e715b377e7b7bde474cb8105165d77474df4c3619b10b3"}, + {file = "Flask_Migrate-4.0.4-py3-none-any.whl", hash = "sha256:77580f27ab39bc68be4906a43c56d7674b45075bc4f883b1d0b985db5164d58f"}, ] [package.dependencies] -alembic = ">=0.7" +alembic = ">=1.9.0" Flask = ">=0.9" Flask-SQLAlchemy = ">=1.0" @@ -845,7 +1013,6 @@ Flask-SQLAlchemy = ">=1.0" name = "flask-oidc" version = "1.4.0" description = "OpenID Connect extension for Flask" -category = "main" optional = false python-versions = "*" files = [ @@ -862,7 +1029,6 @@ six = "*" name = "flask-openid" version = "1.3.0" description = "OpenID support for Flask" -category = "main" optional = false python-versions = ">=3.0" files = [ @@ -876,25 +1042,23 @@ python3-openid = ">=2.0" [[package]] name = "flask-sqlalchemy" -version = "2.5.1" -description = "Adds SQLAlchemy support to your Flask application." -category = "main" +version = "3.0.5" +description = "Add SQLAlchemy support to your Flask application." optional = false -python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*" +python-versions = ">=3.7" files = [ - {file = "Flask-SQLAlchemy-2.5.1.tar.gz", hash = "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912"}, - {file = "Flask_SQLAlchemy-2.5.1-py2.py3-none-any.whl", hash = "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390"}, + {file = "flask_sqlalchemy-3.0.5-py3-none-any.whl", hash = "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283"}, + {file = "flask_sqlalchemy-3.0.5.tar.gz", hash = "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1"}, ] [package.dependencies] -Flask = ">=0.10" -SQLAlchemy = ">=0.8.0" +flask = ">=2.2.5" +sqlalchemy = ">=1.4.18" [[package]] name = "flask-testing" version = "0.8.1" description = "Unit testing for Flask" -category = "main" optional = false python-versions = "*" files = [ @@ -908,7 +1072,6 @@ Flask = "*" name = "flask-wtf" version = "1.1.1" description = "Form rendering, validation, and CSRF protection for Flask with WTForms." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -924,52 +1087,153 @@ WTForms = "*" [package.extras] email = ["email-validator"] +[[package]] +name = "frozenlist" +version = "1.3.3" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.7" +files = [ + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, + {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, + {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, + {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, + {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, + {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, + {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, + {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, + {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, + {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, + {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, +] + [[package]] name = "gevent" -version = "21.12.0" +version = "22.10.2" description = "Coroutine-based network library" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5" files = [ - {file = "gevent-21.12.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:2afa3f3ad528155433f6ac8bd64fa5cc303855b97004416ec719a6b1ca179481"}, - {file = "gevent-21.12.0-cp27-cp27m-win32.whl", hash = "sha256:177f93a3a90f46a5009e0841fef561601e5c637ba4332ab8572edd96af650101"}, - {file = "gevent-21.12.0-cp27-cp27m-win_amd64.whl", hash = "sha256:a5ad4ed8afa0a71e1927623589f06a9b5e8b5e77810be3125cb4d93050d3fd1f"}, - {file = "gevent-21.12.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:eae3c46f9484eaacd67ffcdf4eaf6ca830f587edd543613b0f5c4eb3c11d052d"}, - {file = "gevent-21.12.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1899b921219fc8959ff9afb94dae36be82e0769ed13d330a393594d478a0b3a"}, - {file = "gevent-21.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21cb5c9f4e14d75b3fe0b143ec875d7dbd1495fad6d49704b00e57e781ee0f"}, - {file = "gevent-21.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:542ae891e2aa217d2cf6d8446538fcd2f3263a40eec123b970b899bac391c47a"}, - {file = "gevent-21.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397"}, - {file = "gevent-21.12.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da8d2d51a49b2a5beb02ad619ca9ddbef806ef4870ba04e5ac7b8b41a5b61db3"}, - {file = "gevent-21.12.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfff82f05f14b7f5d9ed53ccb7a609ae8604df522bb05c971bca78ec9d8b2b9"}, - {file = "gevent-21.12.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7909780f0cf18a1fc32aafd8c8e130cdd93c6e285b11263f7f2d1a0f3678bc50"}, - {file = "gevent-21.12.0-cp36-cp36m-win32.whl", hash = "sha256:bb5cb8db753469c7a9a0b8a972d2660fe851aa06eee699a1ca42988afb0aaa02"}, - {file = "gevent-21.12.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c43f081cbca41d27fd8fef9c6a32cf83cb979345b20abc07bf68df165cdadb24"}, - {file = "gevent-21.12.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:74fc1ef16b86616cfddcc74f7292642b0f72dde4dd95aebf4c45bb236744be54"}, - {file = "gevent-21.12.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc2fef0f98ee180704cf95ec84f2bc2d86c6c3711bb6b6740d74e0afe708b62c"}, - {file = "gevent-21.12.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08b4c17064e28f4eb85604486abc89f442c7407d2aed249cf54544ce5c9baee6"}, - {file = "gevent-21.12.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:973749bacb7bc4f4181a8fb2a7e0e2ff44038de56d08e856dd54a5ac1d7331b4"}, - {file = "gevent-21.12.0-cp37-cp37m-win32.whl", hash = "sha256:6a02a88723ed3f0fd92cbf1df3c4cd2fbd87d82b0a4bac3e36a8875923115214"}, - {file = "gevent-21.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f289fae643a3f1c3b909d6b033e6921b05234a4907e9c9c8c3f1fe403e6ac452"}, - {file = "gevent-21.12.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:3baeeccc4791ba3f8db27179dff11855a8f9210ddd754f6c9b48e0d2561c2aea"}, - {file = "gevent-21.12.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05c5e8a50cd6868dd36536c92fb4468d18090e801bd63611593c0717bab63692"}, - {file = "gevent-21.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d86438ede1cbe0fde6ef4cc3f72bf2f1ecc9630d8b633ff344a3aeeca272cdd"}, - {file = "gevent-21.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01928770972181ad8866ee37ea3504f1824587b188fcab782ef1619ce7538766"}, - {file = "gevent-21.12.0-cp38-cp38-win32.whl", hash = "sha256:3c012c73e6c61f13c75e3a4869dbe6a2ffa025f103421a6de9c85e627e7477b1"}, - {file = "gevent-21.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:b7709c64afa8bb3000c28bb91ec42c79594a7cb0f322e20427d57f9762366a5b"}, - {file = "gevent-21.12.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ec21f9eaaa6a7b1e62da786132d6788675b314f25f98d9541f1bf00584ed4749"}, - {file = "gevent-21.12.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22ce1f38fdfe2149ffe8ec2131ca45281791c1e464db34b3b4321ae9d8d2efbb"}, - {file = "gevent-21.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ccffcf708094564e442ac6fde46f0ae9e40015cb69d995f4b39cc29a7643881"}, - {file = "gevent-21.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24d3550fbaeef5fddd794819c2853bca45a86c3d64a056a2c268d981518220d1"}, - {file = "gevent-21.12.0-cp39-cp39-win32.whl", hash = "sha256:2bcec9f80196c751fdcf389ca9f7141e7b0db960d8465ed79be5e685bfcad682"}, - {file = "gevent-21.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:3dad62f55fad839d498c801e139481348991cee6e1c7706041b5fe096cb6a279"}, - {file = "gevent-21.12.0-pp27-pypy_73-win_amd64.whl", hash = "sha256:9f9652d1e4062d4b5b5a0a49ff679fa890430b5f76969d35dccb2df114c55e0f"}, - {file = "gevent-21.12.0.tar.gz", hash = "sha256:f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e"}, + {file = "gevent-22.10.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:97cd42382421779f5d82ec5007199e8a84aa288114975429e4fd0a98f2290f10"}, + {file = "gevent-22.10.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1e1286a76f15b5e15f1e898731d50529e249529095a032453f2c101af3fde71c"}, + {file = "gevent-22.10.2-cp27-cp27m-win32.whl", hash = "sha256:59b47e81b399d49a5622f0f503c59f1ce57b7705306ea0196818951dfc2f36c8"}, + {file = "gevent-22.10.2-cp27-cp27m-win_amd64.whl", hash = "sha256:1d543c9407a1e4bca11a8932916988cfb16de00366de5bf7bc9e7a3f61e60b18"}, + {file = "gevent-22.10.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4e2f008c82dc54ec94f4de12ca6feea60e419babb48ec145456907ae61625aa4"}, + {file = "gevent-22.10.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:990d7069f14dc40674e0d5cb43c68fd3bad8337048613b9bb94a0c4180ffc176"}, + {file = "gevent-22.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f23d0997149a816a2a9045af29c66f67f405a221745b34cefeac5769ed451db8"}, + {file = "gevent-22.10.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b43d500d7d3c0e03070dee813335bb5315215aa1cf6a04c61093dfdd718640b3"}, + {file = "gevent-22.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b68f4c9e20e47ad49fe797f37f91d5bbeace8765ce2707f979a8d4ec197e4d"}, + {file = "gevent-22.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1f001cac0ba8da76abfeb392a3057f81fab3d67cc916c7df8ea977a44a2cc989"}, + {file = "gevent-22.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:3b7eae8a0653ba95a224faaddf629a913ace408edb67384d3117acf42d7dcf89"}, + {file = "gevent-22.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8f2477e7b0a903a01485c55bacf2089110e5f767014967ba4b287ff390ae2638"}, + {file = "gevent-22.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddaa3e310a8f1a45b5c42cf50b54c31003a3028e7d4e085059090ea0e7a5fddd"}, + {file = "gevent-22.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98bc510e80f45486ef5b806a1c305e0e89f0430688c14984b0dbdec03331f48b"}, + {file = "gevent-22.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877abdb3a669576b1d51ce6a49b7260b2a96f6b2424eb93287e779a3219d20ba"}, + {file = "gevent-22.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d21ad79cca234cdbfa249e727500b0ddcbc7adfff6614a96e6eaa49faca3e4f2"}, + {file = "gevent-22.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e955238f59b2947631c9782a713280dd75884e40e455313b5b6bbc20b92ff73"}, + {file = "gevent-22.10.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5aa99e4882a9e909b4756ee799c6fa0f79eb0542779fad4cc60efa23ec1b2aa8"}, + {file = "gevent-22.10.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:d82081656a5b9a94d37c718c8646c757e1617e389cdc533ea5e6a6f0b8b78545"}, + {file = "gevent-22.10.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f4bfd74c178351a4a05c5c7df6f8a0a279ff6f392b57608ce0e83c768207f9"}, + {file = "gevent-22.10.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ff3796692dff50fec2f381b9152438b221335f557c4f9b811f7ded51b7a25a1"}, + {file = "gevent-22.10.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f01c9adbcb605364694b11dcd0542ec468a29ac7aba2fb5665dc6caf17ba4d7e"}, + {file = "gevent-22.10.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9d85574eb729f981fea9a78998725a06292d90a3ed50ddca74530c3148c0be41"}, + {file = "gevent-22.10.2-cp36-cp36m-win32.whl", hash = "sha256:8c192d2073e558e241f0b592c1e2b34127a4481a5be240cad4796533b88b1a98"}, + {file = "gevent-22.10.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a2237451c721a0f874ef89dbb4af4fdc172b76a964befaa69deb15b8fff10f49"}, + {file = "gevent-22.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:53ee7f170ed42c7561fe8aff5d381dc9a4124694e70580d0c02fba6aafc0ea37"}, + {file = "gevent-22.10.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:96c56c280e3c43cfd075efd10b250350ed5ffd3c1514ec99a080b1b92d7c8374"}, + {file = "gevent-22.10.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c144e08dfad4106effc043a026e5d0c0eff6ad031904c70bf5090c63f3a6a7"}, + {file = "gevent-22.10.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:018f93de7d5318d2fb440f846839a4464738468c3476d5c9cf7da45bb71c18bd"}, + {file = "gevent-22.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7ed2346eb9dc4344f9cb0d7963ce5b74fe16fdd031a2809bb6c2b6eba7ebcd5"}, + {file = "gevent-22.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84c517e33ed604fa06b7d756dc0171169cc12f7fdd68eb7b17708a62eebf4516"}, + {file = "gevent-22.10.2-cp37-cp37m-win32.whl", hash = "sha256:4114f0f439f0b547bb6f1d474fee99ddb46736944ad2207cef3771828f6aa358"}, + {file = "gevent-22.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:0d581f22a5be6281b11ad6309b38b18f0638cf896931223cbaa5adb904826ef6"}, + {file = "gevent-22.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2929377c8ebfb6f4d868d161cd8de2ea6b9f6c7a5fcd4f78bcd537319c16190b"}, + {file = "gevent-22.10.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:efc003b6c1481165af61f0aeac248e0a9ac8d880bb3acbe469b448674b2d5281"}, + {file = "gevent-22.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db562a8519838bddad0c439a2b12246bab539dd50e299ea7ff3644274a33b6a5"}, + {file = "gevent-22.10.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1472012493ca1fac103f700d309cb6ef7964dcdb9c788d1768266e77712f5e49"}, + {file = "gevent-22.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c04ee32c11e9fcee47c1b431834878dc987a7a2cc4fe126ddcae3bad723ce89"}, + {file = "gevent-22.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8729129edef2637a8084258cb9ec4e4d5ca45d97ac77aa7a6ff19ccb530ab731"}, + {file = "gevent-22.10.2-cp38-cp38-win32.whl", hash = "sha256:ae90226074a6089371a95f20288431cd4b3f6b0b096856afd862e4ac9510cddd"}, + {file = "gevent-22.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:494c7f29e94df9a1c3157d67bb7edfa32a46eed786e04d9ee68d39f375e30001"}, + {file = "gevent-22.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:58898dbabb5b11e4d0192aae165ad286dc6742c543e1be9d30dc82753547c508"}, + {file = "gevent-22.10.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:4197d423e198265eef39a0dea286ef389da9148e070310f34455ecee8172c391"}, + {file = "gevent-22.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da4183f0b9d9a1e25e1758099220d32c51cc2c6340ee0dea3fd236b2b37598e4"}, + {file = "gevent-22.10.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5488eba6a568b4d23c072113da4fc0feb1b5f5ede7381656dc913e0d82204e2"}, + {file = "gevent-22.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319d8b1699b7b8134de66d656cd739b308ab9c45ace14d60ae44de7775b456c9"}, + {file = "gevent-22.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f3329bedbba4d3146ae58c667e0f9ac1e6f1e1e6340c7593976cdc60aa7d1a47"}, + {file = "gevent-22.10.2-cp39-cp39-win32.whl", hash = "sha256:172caa66273315f283e90a315921902cb6549762bdcb0587fd60cb712a9d6263"}, + {file = "gevent-22.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:323b207b281ba0405fea042067fa1a61662e5ac0d574ede4ebbda03efd20c350"}, + {file = "gevent-22.10.2-pp27-pypy_73-win_amd64.whl", hash = "sha256:ed7f16613eebf892a6a744d7a4a8f345bc6f066a0ff3b413e2479f9c0a180193"}, + {file = "gevent-22.10.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a47a4e77e2bc668856aad92a0b8de7ee10768258d93cd03968e6c7ba2e832f76"}, + {file = "gevent-22.10.2.tar.gz", hash = "sha256:1ca01da176ee37b3527a2702f7d40dbc9ffb8cfc7be5a03bfa4f9eec45e55c46"}, ] [package.dependencies] cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} -greenlet = {version = ">=1.1.0,<2.0", markers = "platform_python_implementation == \"CPython\""} +greenlet = {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\""} setuptools = "*" "zope.event" = "*" "zope.interface" = "*" @@ -983,88 +1247,81 @@ test = ["backports.socketpair", "cffi (>=1.12.2)", "contextvars (==2.4)", "cover [[package]] name = "greenlet" -version = "1.1.3.post0" +version = "2.0.2" description = "Lightweight in-process concurrent programming" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ - {file = "greenlet-1.1.3.post0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:949c9061b8c6d3e6e439466a9be1e787208dec6246f4ec5fffe9677b4c19fcc3"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d7815e1519a8361c5ea2a7a5864945906f8e386fa1bc26797b4d443ab11a4589"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9649891ab4153f217f319914455ccf0b86986b55fc0573ce803eb998ad7d6854"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-win32.whl", hash = "sha256:11fc7692d95cc7a6a8447bb160d98671ab291e0a8ea90572d582d57361360f05"}, - {file = "greenlet-1.1.3.post0-cp27-cp27m-win_amd64.whl", hash = "sha256:05ae7383f968bba4211b1fbfc90158f8e3da86804878442b4fb6c16ccbcaa519"}, - {file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ccbe7129a282ec5797df0451ca1802f11578be018a32979131065565da89b392"}, - {file = "greenlet-1.1.3.post0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a8b58232f5b72973350c2b917ea3df0bebd07c3c82a0a0e34775fc2c1f857e9"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:f6661b58412879a2aa099abb26d3c93e91dedaba55a6394d1fb1512a77e85de9"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c6e942ca9835c0b97814d14f78da453241837419e0d26f7403058e8db3e38f8"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a812df7282a8fc717eafd487fccc5ba40ea83bb5b13eb3c90c446d88dbdfd2be"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a7a6560df073ec9de2b7cb685b199dfd12519bc0020c62db9d1bb522f989fa"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:17a69967561269b691747e7f436d75a4def47e5efcbc3c573180fc828e176d80"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:60839ab4ea7de6139a3be35b77e22e0398c270020050458b3d25db4c7c394df5"}, - {file = "greenlet-1.1.3.post0-cp310-cp310-win_amd64.whl", hash = "sha256:8926a78192b8b73c936f3e87929931455a6a6c6c385448a07b9f7d1072c19ff3"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c6f90234e4438062d6d09f7d667f79edcc7c5e354ba3a145ff98176f974b8132"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814f26b864ed2230d3a7efe0336f5766ad012f94aad6ba43a7c54ca88dd77cba"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fda1139d87ce5f7bd80e80e54f9f2c6fe2f47983f1a6f128c47bf310197deb6"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0643250dd0756f4960633f5359884f609a234d4066686754e834073d84e9b51"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cb863057bed786f6622982fb8b2c122c68e6e9eddccaa9fa98fd937e45ee6c4f"}, - {file = "greenlet-1.1.3.post0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8c0581077cf2734569f3e500fab09c0ff6a2ab99b1afcacbad09b3c2843ae743"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:695d0d8b5ae42c800f1763c9fce9d7b94ae3b878919379150ee5ba458a460d57"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5662492df0588a51d5690f6578f3bbbd803e7f8d99a99f3bf6128a401be9c269"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:bffba15cff4802ff493d6edcf20d7f94ab1c2aee7cfc1e1c7627c05f1102eee8"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-win32.whl", hash = "sha256:7afa706510ab079fd6d039cc6e369d4535a48e202d042c32e2097f030a16450f"}, - {file = "greenlet-1.1.3.post0-cp35-cp35m-win_amd64.whl", hash = "sha256:3a24f3213579dc8459e485e333330a921f579543a5214dbc935bc0763474ece3"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:64e10f303ea354500c927da5b59c3802196a07468332d292aef9ddaca08d03dd"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:eb6ac495dccb1520667cfea50d89e26f9ffb49fa28496dea2b95720d8b45eb54"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:88720794390002b0c8fa29e9602b395093a9a766b229a847e8d88349e418b28a"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39464518a2abe9c505a727af7c0b4efff2cf242aa168be5f0daa47649f4d7ca8"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0914f02fcaa8f84f13b2df4a81645d9e82de21ed95633765dd5cc4d3af9d7403"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96656c5f7c95fc02c36d4f6ef32f4e94bb0b6b36e6a002c21c39785a4eec5f5d"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4f74aa0092602da2069df0bc6553919a15169d77bcdab52a21f8c5242898f519"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3aeac044c324c1a4027dca0cde550bd83a0c0fbff7ef2c98df9e718a5086c194"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-win32.whl", hash = "sha256:fe7c51f8a2ab616cb34bc33d810c887e89117771028e1e3d3b77ca25ddeace04"}, - {file = "greenlet-1.1.3.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:70048d7b2c07c5eadf8393e6398595591df5f59a2f26abc2f81abca09610492f"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:66aa4e9a726b70bcbfcc446b7ba89c8cec40f405e51422c39f42dfa206a96a05"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:025b8de2273d2809f027d347aa2541651d2e15d593bbce0d5f502ca438c54136"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:82a38d7d2077128a017094aff334e67e26194f46bd709f9dcdacbf3835d47ef5"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7d20c3267385236b4ce54575cc8e9f43e7673fc761b069c820097092e318e3b"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8ece5d1a99a2adcb38f69af2f07d96fb615415d32820108cd340361f590d128"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2794eef1b04b5ba8948c72cc606aab62ac4b0c538b14806d9c0d88afd0576d6b"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a8d24eb5cb67996fb84633fdc96dbc04f2d8b12bfcb20ab3222d6be271616b67"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0120a879aa2b1ac5118bce959ea2492ba18783f65ea15821680a256dfad04754"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-win32.whl", hash = "sha256:bef49c07fcb411c942da6ee7d7ea37430f830c482bf6e4b72d92fd506dd3a427"}, - {file = "greenlet-1.1.3.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:62723e7eb85fa52e536e516ee2ac91433c7bb60d51099293671815ff49ed1c21"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d25cdedd72aa2271b984af54294e9527306966ec18963fd032cc851a725ddc1b"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:924df1e7e5db27d19b1359dc7d052a917529c95ba5b8b62f4af611176da7c8ad"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ec615d2912b9ad807afd3be80bf32711c0ff9c2b00aa004a45fd5d5dde7853d9"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0971d37ae0eaf42344e8610d340aa0ad3d06cd2eee381891a10fe771879791f9"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325f272eb997916b4a3fc1fea7313a8adb760934c2140ce13a2117e1b0a8095d"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75afcbb214d429dacdf75e03a1d6d6c5bd1fa9c35e360df8ea5b6270fb2211c"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5c2d21c2b768d8c86ad935e404cc78c30d53dea009609c3ef3a9d49970c864b5"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:467b73ce5dcd89e381292fb4314aede9b12906c18fab903f995b86034d96d5c8"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-win32.whl", hash = "sha256:8149a6865b14c33be7ae760bcdb73548bb01e8e47ae15e013bf7ef9290ca309a"}, - {file = "greenlet-1.1.3.post0-cp38-cp38-win_amd64.whl", hash = "sha256:104f29dd822be678ef6b16bf0035dcd43206a8a48668a6cae4d2fe9c7a7abdeb"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:c8c9301e3274276d3d20ab6335aa7c5d9e5da2009cccb01127bddb5c951f8870"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8415239c68b2ec9de10a5adf1130ee9cb0ebd3e19573c55ba160ff0ca809e012"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:3c22998bfef3fcc1b15694818fc9b1b87c6cc8398198b96b6d355a7bcb8c934e"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa1845944e62f358d63fcc911ad3b415f585612946b8edc824825929b40e59e"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:890f633dc8cb307761ec566bc0b4e350a93ddd77dc172839be122be12bae3e10"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cf37343e43404699d58808e51f347f57efd3010cc7cee134cdb9141bd1ad9ea"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5edf75e7fcfa9725064ae0d8407c849456553a181ebefedb7606bac19aa1478b"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a954002064ee919b444b19c1185e8cce307a1f20600f47d6f4b6d336972c809"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-win32.whl", hash = "sha256:2ccdc818cc106cc238ff7eba0d71b9c77be868fdca31d6c3b1347a54c9b187b2"}, - {file = "greenlet-1.1.3.post0-cp39-cp39-win_amd64.whl", hash = "sha256:91a84faf718e6f8b888ca63d0b2d6d185c8e2a198d2a7322d75c303e7097c8b7"}, - {file = "greenlet-1.1.3.post0.tar.gz", hash = "sha256:f5e09dc5c6e1796969fd4b775ea1417d70e49a5df29aaa8e5d10675d9e11872c"}, + {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, + {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, + {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, + {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, + {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, + {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, + {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, + {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, + {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, + {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, + {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, + {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, + {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, + {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, + {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, + {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, + {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, + {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, + {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, + {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, + {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, + {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, ] [package.extras] -docs = ["Sphinx"] +docs = ["Sphinx", "docutils (<0.18)"] +test = ["objgraph", "psutil"] [[package]] name = "gunicorn" version = "20.1.0" description = "WSGI HTTP Server for UNIX" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1085,7 +1342,6 @@ tornado = ["tornado (>=0.2)"] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1097,7 +1353,6 @@ files = [ name = "html5lib" version = "1.1" description = "HTML parser based on the WHATWG HTML specification" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1119,7 +1374,6 @@ lxml = ["lxml"] name = "httpagentparser" version = "1.9.5" description = "Extracts OS Browser etc information from http user agent string" -category = "main" optional = false python-versions = "*" files = [ @@ -1130,7 +1384,6 @@ files = [ name = "httplib2" version = "0.22.0" description = "A comprehensive HTTP client library." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1145,7 +1398,6 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 name = "humanize" version = "4.7.0" description = "Python humanize utilities" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1160,7 +1412,6 @@ tests = ["freezegun", "pytest", "pytest-cov"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1172,7 +1423,6 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1184,7 +1434,6 @@ files = [ name = "isodate" version = "0.6.1" description = "An ISO 8601 date/time/duration parser and formatter" -category = "main" optional = false python-versions = "*" files = [ @@ -1199,7 +1448,6 @@ six = "*" name = "itsdangerous" version = "2.1.2" description = "Safely pass data to untrusted environments and back." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1211,7 +1459,6 @@ files = [ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1227,40 +1474,40 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "kombu" -version = "4.6.11" +version = "5.3.1" description = "Messaging library for Python." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.8" files = [ - {file = "kombu-4.6.11-py2.py3-none-any.whl", hash = "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a"}, - {file = "kombu-4.6.11.tar.gz", hash = "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"}, + {file = "kombu-5.3.1-py3-none-any.whl", hash = "sha256:48ee589e8833126fd01ceaa08f8a2041334e9f5894e5763c8486a550454551e9"}, + {file = "kombu-5.3.1.tar.gz", hash = "sha256:fbd7572d92c0bf71c112a6b45163153dea5a7b6a701ec16b568c27d0fd2370f2"}, ] [package.dependencies] -amqp = ">=2.6.0,<2.7" +amqp = ">=5.1.1,<6.0.0" +vine = "*" [package.extras] -azureservicebus = ["azure-servicebus (>=0.21.1)"] -azurestoragequeues = ["azure-storage-queue"] -consul = ["python-consul (>=0.6.0)"] -librabbitmq = ["librabbitmq (>=1.5.2)"] -mongodb = ["pymongo (>=3.3.0)"] +azureservicebus = ["azure-servicebus (>=7.10.0)"] +azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] +confluentkafka = ["confluent-kafka (==2.1.1)"] +consul = ["python-consul2"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +mongodb = ["pymongo (>=4.1.1)"] msgpack = ["msgpack"] pyro = ["pyro4"] qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] -redis = ["redis (>=3.3.11)"] +redis = ["redis (>=4.5.2)"] slmq = ["softlayer-messaging (>=1.0.3)"] -sqlalchemy = ["sqlalchemy"] -sqs = ["boto3 (>=1.4.4)", "pycurl (==7.43.0.2)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] -zookeeper = ["kazoo (>=1.3.1)"] +zookeeper = ["kazoo (>=2.8.0)"] [[package]] name = "langcodes" version = "3.3.0" description = "Tools for labeling human languages with IETF language tags" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1278,7 +1525,6 @@ data = ["language-data (>=1.1,<2.0)"] name = "language-data" version = "1.1" description = "Supplementary data about languages used by the langcodes module" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1291,95 +1537,102 @@ marisa-trie = ">=0.7.7,<0.8.0" [[package]] name = "libsass" -version = "0.21.0" +version = "0.22.0" description = "Sass for Python: A straightforward binding of libsass for Python." -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "libsass-0.21.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:06c8776417fe930714bdc930a3d7e795ae3d72be6ac883ff72a1b8f7c49e5ffb"}, - {file = "libsass-0.21.0-cp27-cp27m-win32.whl", hash = "sha256:a005f298f64624f313a3ac618ab03f844c71d84ae4f4a4aec4b68d2a4ffe75eb"}, - {file = "libsass-0.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:6b984510ed94993708c0d697b4fef2d118929bbfffc3b90037be0f5ccadf55e7"}, - {file = "libsass-0.21.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e25dd9047a9392d3c59a0b869e0404f2b325a03871ee45285ee33b3664f5613"}, - {file = "libsass-0.21.0-cp36-abi3-macosx_10_14_x86_64.whl", hash = "sha256:12f39712de38689a8b785b7db41d3ba2ea1d46f9379d81ea4595802d91fa6529"}, - {file = "libsass-0.21.0-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e2b1a7d093f2e76dc694c17c0c285e846d0b0deb0e8b21dc852ba1a3a4e2f1d6"}, - {file = "libsass-0.21.0-cp36-abi3-win32.whl", hash = "sha256:abc29357ee540849faf1383e1746d40d69ed5cb6d4c346df276b258f5aa8977a"}, - {file = "libsass-0.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:659ae41af8708681fa3ec73f47b9735a6725e71c3b66ff570bfce78952f2314e"}, - {file = "libsass-0.21.0-cp38-abi3-macosx_12_0_arm64.whl", hash = "sha256:c9ec490609752c1d81ff6290da33485aa7cb6d7365ac665b74464c1b7d97f7da"}, - {file = "libsass-0.21.0.tar.gz", hash = "sha256:d5ba529d9ce668be9380563279f3ffe988f27bc5b299c5a28453df2e0b0fbaf2"}, + {file = "libsass-0.22.0-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f1efc1b612299c88aec9e39d6ca0c266d360daa5b19d9430bdeaffffa86993f9"}, + {file = "libsass-0.22.0-cp37-abi3-macosx_10_15_x86_64.whl", hash = "sha256:081e256ab3c5f3f09c7b8dea3bf3bf5e64a97c6995fd9eea880639b3f93a9f9a"}, + {file = "libsass-0.22.0-cp37-abi3-win32.whl", hash = "sha256:89c5ce497fcf3aba1dd1b19aae93b99f68257e5f2026b731b00a872f13324c7f"}, + {file = "libsass-0.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:65455a2728b696b62100eb5932604aa13a29f4ac9a305d95773c14aaa7200aaf"}, + {file = "libsass-0.22.0.tar.gz", hash = "sha256:3ab5ad18e47db560f4f0c09e3d28cf3bb1a44711257488ac2adad69f4f7f8425"}, ] -[package.dependencies] -six = "*" - [[package]] name = "lxml" -version = "4.6.5" +version = "4.9.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ - {file = "lxml-4.6.5-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2"}, - {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b"}, - {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808"}, - {file = "lxml-4.6.5-cp27-cp27m-win32.whl", hash = "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c"}, - {file = "lxml-4.6.5-cp27-cp27m-win_amd64.whl", hash = "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71"}, - {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93"}, - {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352"}, - {file = "lxml-4.6.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1"}, - {file = "lxml-4.6.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5"}, - {file = "lxml-4.6.5-cp310-cp310-win32.whl", hash = "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952"}, - {file = "lxml-4.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0"}, - {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203"}, - {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4"}, - {file = "lxml-4.6.5-cp35-cp35m-win32.whl", hash = "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b"}, - {file = "lxml-4.6.5-cp35-cp35m-win_amd64.whl", hash = "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408"}, - {file = "lxml-4.6.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c"}, - {file = "lxml-4.6.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3"}, - {file = "lxml-4.6.5-cp36-cp36m-win32.whl", hash = "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04"}, - {file = "lxml-4.6.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120"}, - {file = "lxml-4.6.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2"}, - {file = "lxml-4.6.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9"}, - {file = "lxml-4.6.5-cp37-cp37m-win32.whl", hash = "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090"}, - {file = "lxml-4.6.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f"}, - {file = "lxml-4.6.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44"}, - {file = "lxml-4.6.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9"}, - {file = "lxml-4.6.5-cp38-cp38-win32.whl", hash = "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d"}, - {file = "lxml-4.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41"}, - {file = "lxml-4.6.5-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace"}, - {file = "lxml-4.6.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542"}, - {file = "lxml-4.6.5-cp39-cp39-win32.whl", hash = "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b"}, - {file = "lxml-4.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e"}, - {file = "lxml-4.6.5.tar.gz", hash = "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca"}, + {file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"}, + {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"}, + {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"}, + {file = "lxml-4.9.2-cp27-cp27m-win32.whl", hash = "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de"}, + {file = "lxml-4.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3"}, + {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50"}, + {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975"}, + {file = "lxml-4.9.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c"}, + {file = "lxml-4.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a"}, + {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4"}, + {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4"}, + {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7"}, + {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184"}, + {file = "lxml-4.9.2-cp310-cp310-win32.whl", hash = "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda"}, + {file = "lxml-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab"}, + {file = "lxml-4.9.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9"}, + {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf"}, + {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380"}, + {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92"}, + {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1"}, + {file = "lxml-4.9.2-cp311-cp311-win32.whl", hash = "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33"}, + {file = "lxml-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd"}, + {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"}, + {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"}, + {file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"}, + {file = "lxml-4.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5"}, + {file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1"}, + {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e"}, + {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"}, + {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"}, + {file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"}, + {file = "lxml-4.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3"}, + {file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894"}, + {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45"}, + {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e"}, + {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b"}, + {file = "lxml-4.9.2-cp37-cp37m-win32.whl", hash = "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe"}, + {file = "lxml-4.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9"}, + {file = "lxml-4.9.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03"}, + {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c"}, + {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f"}, + {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457"}, + {file = "lxml-4.9.2-cp38-cp38-win32.whl", hash = "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b"}, + {file = "lxml-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7"}, + {file = "lxml-4.9.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947"}, + {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5"}, + {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5"}, + {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2"}, + {file = "lxml-4.9.2-cp39-cp39-win32.whl", hash = "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1"}, + {file = "lxml-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f"}, + {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c"}, + {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f"}, + {file = "lxml-4.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7"}, + {file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"}, + {file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"}, ] [package.extras] @@ -1392,7 +1645,6 @@ source = ["Cython (>=0.29.7)"] name = "mailmanclient" version = "3.3.5" description = "mailmanclient -- Python bindings for Mailman REST API" -category = "main" optional = false python-versions = "*" files = [ @@ -1411,7 +1663,6 @@ testing = ["falcon (>1.4.1)", "httpx", "mailman (>=3.3.1)", "pytest", "pytest-se name = "mako" version = "1.2.4" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1431,7 +1682,6 @@ testing = ["pytest"] name = "marisa-trie" version = "0.7.8" description = "Static memory-efficient and fast Trie-like structures for Python." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1510,7 +1760,6 @@ test = ["hypothesis", "pytest", "readme-renderer"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1568,30 +1817,28 @@ files = [ [[package]] name = "marshmallow" -version = "3.20.1" +version = "3.19.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "marshmallow-3.20.1-py3-none-any.whl", hash = "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c"}, - {file = "marshmallow-3.20.1.tar.gz", hash = "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889"}, + {file = "marshmallow-3.19.0-py3-none-any.whl", hash = "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b"}, + {file = "marshmallow-3.19.0.tar.gz", hash = "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78"}, ] [package.dependencies] packaging = ">=17.0" [package.extras] -dev = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] -docs = ["alabaster (==0.7.13)", "autodocsumm (==0.2.11)", "sphinx (==7.0.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] -lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)"] +dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.3.0)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)"] tests = ["pytest", "pytz", "simplejson"] [[package]] name = "marshmallow-enum" version = "1.5.1" description = "Enum field for Marshmallow" -category = "main" optional = false python-versions = "*" files = [ @@ -1606,7 +1853,6 @@ marshmallow = ">=2.0.0" name = "marshmallow-union" version = "0.1.15.post1" description = "Union fields for marshmallow." -category = "main" optional = false python-versions = "*" files = [ @@ -1619,144 +1865,223 @@ marshmallow = ">=3.0.0" [[package]] name = "mmh3" -version = "3.1.0" -description = "Python wrapper for MurmurHash (MurmurHash3), a set of fast and robust hash functions." -category = "main" +version = "4.0.0" +description = "Python extension for MurmurHash (MurmurHash3), a set of fast and robust hash functions." optional = false python-versions = "*" files = [ - {file = "mmh3-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:16ee043b1bac040b4324b8baee39df9fdca480a560a6d74f2eef66a5009a234e"}, - {file = "mmh3-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04ac865319e5b36148a4b6cdf27f8bda091c47c4ab7b355d7f353dfc2b8a3cce"}, - {file = "mmh3-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e751f5433417a21c2060b0efa1afc67cfbe29977c867336148c8edb086fae70"}, - {file = "mmh3-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdb863b89c1b34e3681d4a3b15d424734940eb8036f3457cb35ef34fb87a503c"}, - {file = "mmh3-3.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1230930fbf2faec4ddf5b76d0768ae73c102de173c301962bdd468177275adf9"}, - {file = "mmh3-3.1.0-cp310-cp310-win32.whl", hash = "sha256:b8ed7a2361718795a1b519a08d05f44947a20b27e202b53946561a00dde669c1"}, - {file = "mmh3-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:29e878e7467a000f34ab68c218ad7ad81312c0a94bc10df3c50a48bcad39dd83"}, - {file = "mmh3-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c271472325b70d64a4fbb1f2e964ca5b093ac10258e1390f8408890b065868fe"}, - {file = "mmh3-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0109320f7e0e262123ff4f1acd06acfbc8b3bf19cc13d98c0bc369264430aaeb"}, - {file = "mmh3-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:524e29dfe66499695f9496edcfc96782d130aabd6ba12c50c72372163cc6f3ea"}, - {file = "mmh3-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66bdb06a03074e65e614da1aa199b1d16c90608bec9d8fc3faa81d887ffe93cc"}, - {file = "mmh3-3.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a4d471eb75df8320061ab3b8cbe11c970be9f116b01bc2222ebda9c0a777520"}, - {file = "mmh3-3.1.0-cp311-cp311-win32.whl", hash = "sha256:a886d9ce995a4bdfd7a600ddf61b9015cccbc73c50b898f8ff3c78af24384710"}, - {file = "mmh3-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:5edb5ac882c04aff8a2a18ae8b74a0c339ac9b83db9820d8456f518bb558e0d8"}, - {file = "mmh3-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:190fd10981fbd6c67e10ce3b56bcc021562c0df0fee2e2864347d64e65b1783a"}, - {file = "mmh3-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd781b115cf649811cfde76368c33d2e553b6f88bb41131c314f30d8e65e9d24"}, - {file = "mmh3-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f48bb0a867077acc1f548591ad49506389f36d18f36dccd10becf071e5cbdda4"}, - {file = "mmh3-3.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d0936a82438e340636a11b9a938378870fc1c7a139632dac09a9a9277351704"}, - {file = "mmh3-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:d196cc035c2238493248522ae4e54c3cb790549b1564f6dea4d88dfe4b326313"}, - {file = "mmh3-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:731d37f089b6c212fab1beea24e673161146eb6c76baf9ac074a3424d1172d41"}, - {file = "mmh3-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9977fb81f8c66f4eee8439734a18dba7826fe78723d15ab53f42db977005be0f"}, - {file = "mmh3-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bf4f3f20a8b8405c08b13bc9e4ac33bf55129b50b535cd07ce1891b7f96326ac"}, - {file = "mmh3-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87cdbc6e70099ad92f17a28b4054ffb1938657e8fb7c1e4e03b194a1b4683fd6"}, - {file = "mmh3-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6dd81321d14f62aa3711f30533c85a74dc7596e0fee63c8eddd375bc92ab846c"}, - {file = "mmh3-3.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e6eba88e5c1a2778f3de00a9502e3c214ebb757337ece2a7d71e060d188ddfa"}, - {file = "mmh3-3.1.0-cp38-cp38-win32.whl", hash = "sha256:d91e696925f208d28f3bb7bdf29815524ce955248276af256519bd3538c411ce"}, - {file = "mmh3-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:cbc2917df568aeb86ec5aa863bfb20fa14e01039cbdce7650efbabc30960df49"}, - {file = "mmh3-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b22832d565128be83d69f5d49243bb567840a954df377c9f5b26646a6eec39b"}, - {file = "mmh3-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ced92a0e285a9111413541c197b0c17d280cee96f7c564b258caf5de5ab8ee01"}, - {file = "mmh3-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f906833753b4ddcb690c2c1b74e77725868bc3a8b762b7a77737d08be89ae41d"}, - {file = "mmh3-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b5685832a7a87a55ebff481794bc410484d7bd4c5e80dae4d8ac50739138ef"}, - {file = "mmh3-3.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d2aa4d422c7c088bbc5d367b45431268ebe6742a0a64eade93fab708e25757c"}, - {file = "mmh3-3.1.0-cp39-cp39-win32.whl", hash = "sha256:4459bec818f534dc8378568ad89ab310ff47cda3e00ab322edce48dd899bba32"}, - {file = "mmh3-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:03e04b3480e71828f48d17653451a3286555f0534942cb6ba93065b10ad5f9dc"}, - {file = "mmh3-3.1.0.tar.gz", hash = "sha256:9b0f2b2ab4a915333c9d1089572e290a021ebb5b900bb7f7114dccc03995d732"}, + {file = "mmh3-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:214ae1ab976401b3fd8da2a3828d1520d31592efacab63f90c222a0e69ad68cf"}, + {file = "mmh3-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ab345fba03d94cd08494a60c244085fb800645881639795b9390900672ee1c4"}, + {file = "mmh3-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:072d449ed3769b7faff5ce7fe05323d2602834f03dfc3969dcedb183b1d902ec"}, + {file = "mmh3-4.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e78c400595fb10c5ba46bd2386a0f1b2d5345c09d391882f96a27b4cf8bfc84"}, + {file = "mmh3-4.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec5c0e0f4be15d73d2ff8d00212c67fcd4f36bca4b43e3c940c545341805dd22"}, + {file = "mmh3-4.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:006ac0d3fe9cbc2855f63491ac3ce77290cedb6adf147d9bf803eb4097f765a5"}, + {file = "mmh3-4.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dceabcf98a3c5c9a137a81c7e2f8cb9b438c1b1da6d8c4d7ec8ca9df52cdd3c"}, + {file = "mmh3-4.0.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba8734f408a08822b2597a70a58549fd84a7afd86a18839ae1bb0bc9b9d2b816"}, + {file = "mmh3-4.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2c2642101105a36db0f936f137cd27bdc75335739e06d0d172d1ece907cb99e"}, + {file = "mmh3-4.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7754d661e8dd855b7950cb94f33db45ee53f12bc271eb376d5a8c84c4c039ad4"}, + {file = "mmh3-4.0.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c73a503ab4297b4a2125eeda50b28bcd7eb8a6eef7b52e2a4a4c810267d0076"}, + {file = "mmh3-4.0.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6dd94d8650576b1395640dfa62f49aa5bdead7200a655efee215b4a9c78f4882"}, + {file = "mmh3-4.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:496e39cc349630294fbf39db7571d5833e9e779df90fc0cc1f652c2f1df7b1c1"}, + {file = "mmh3-4.0.0-cp310-cp310-win32.whl", hash = "sha256:4333668e3ddadd1795c223b44376f18b22fec205fd9604ae7b8e7ab3cdf12ce0"}, + {file = "mmh3-4.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:c6af081268d05a645c9195968baf901d1a70f64e05586c9393f744ce0943a6d8"}, + {file = "mmh3-4.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:14e835ea27fe36bf9fe584dbd2063c9c3c68e3bb3d11c41c5922bf5b9759b2e0"}, + {file = "mmh3-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dfbd7db8ed8ce8ab0cb1dee1a25f15a900c9e66ef12004ab593984a51ba36fae"}, + {file = "mmh3-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e39f5aa5ce2cda81f1f5856092bdb8e9263cb021990c1aede92c31c84a593ef"}, + {file = "mmh3-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:646eb838998f60eea6fbcfde8ddef1081c4a10a30c09400ee2ec57a789cafb9b"}, + {file = "mmh3-4.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b895044e24b845e75af3cb6ec3cd7ae88fa87ffcba93f2bfed3ae0c226d4a2e5"}, + {file = "mmh3-4.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd912d31aa5e2327342777f8b9cde8b82adc3525fd7fa535a2b7b76789f4162a"}, + {file = "mmh3-4.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a0323dfd7609634867e5fb23cf21f96c36792b558d88af73fdbc97e3440ea79"}, + {file = "mmh3-4.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2107a8de64e79d7b532ae9844e8ce79ff3284d5b6661b0566e5511cf11c6efca"}, + {file = "mmh3-4.0.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c6e9e87c4ee08b60c5b5f967ac82777974b2aa0df5ef6f574c66a775231470"}, + {file = "mmh3-4.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:54064f72545c187150edbbf2e6c5c39017797d851f99aee3726ce5dc26d786f2"}, + {file = "mmh3-4.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1a9e6244923549e23d4b88c36d144d326092e0d33b802295d04c76e5dde567f4"}, + {file = "mmh3-4.0.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:087ee4a8c6dbfb4f356aaf32879846ad13e92894201f4d221a087fbb8857ec9a"}, + {file = "mmh3-4.0.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e6e5e4f543bf798161321deb186723a4ef530e7f216ea4c153e525f63c78fc78"}, + {file = "mmh3-4.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df22bd073107fe20006f9197ba29579e915a9a89142ef65130939aa0136c78e7"}, + {file = "mmh3-4.0.0-cp311-cp311-win32.whl", hash = "sha256:f4ec497d5926842a45f235d1eda03ef2cc10e4ccb0738bbe828837817f0fbbfd"}, + {file = "mmh3-4.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a32c815f4d32bcbf71147b9109904e34f33716bed344955d13983026dc8568e0"}, + {file = "mmh3-4.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:72706beb124293b58968881ce3d59f678538c4dec3977c4edd9a9db402ad4486"}, + {file = "mmh3-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:86a8f5af47b78fd5e70e4d48c6da0a04492ab267780ff87e62ddfae5260c3443"}, + {file = "mmh3-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:285751d7e9a98ea842186240ad7a1ca33f0ffa6c0c7e6e3511ac094f8b4b289d"}, + {file = "mmh3-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8a334ce72e7b1602a951ae24453c087f0729049281954f57c2bbc3a3eddee4bb"}, + {file = "mmh3-4.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5638d5d26f7bf1bd79233e5140303ac47c506d51d86f67595ee851f2d49fa480"}, + {file = "mmh3-4.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1755485dc411336c429824258f2fe1a2f53fad2ba1481e922111312d0a698cf4"}, + {file = "mmh3-4.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a7f6089c37b83c5209fec02492418222e0051f09f27bc7fce9d49ee36603cd"}, + {file = "mmh3-4.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5cf444ce11571d0cf65516255b119d3dd733f86120e0acf7371363c97e396d"}, + {file = "mmh3-4.0.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ca5f2ad2af9decee4f9a3c91a823bf71c4f634ff400b829c648c0dc5cd7503f"}, + {file = "mmh3-4.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4c4d482434edf2581ae6bdaf99840791938b089acef56832d13f91e81745bff1"}, + {file = "mmh3-4.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a14c10d50526ee91ca474780f029b97d78fd9bb47ae9c248b8cb1a964525f8f"}, + {file = "mmh3-4.0.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e08ebbdfa008f5d957e69031ab625f7d22313fdff14f845d213ab06f585846e4"}, + {file = "mmh3-4.0.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:2cc0e1872acff360e95f8e3768b5f1777a1ebc700caf88a8a22adb153d24b9fa"}, + {file = "mmh3-4.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2070b169a8b82ad4dcbc6c34fcfd538428322e1f2db5dce349023aad2ba0dc94"}, + {file = "mmh3-4.0.0-cp38-cp38-win32.whl", hash = "sha256:e390c820a31146c73b0cb60da65906a866d4fe43b4688cc3cdf5f33c423d09a1"}, + {file = "mmh3-4.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:55ac731333b5e0e9f93ad56cab95b05ef1621a5a87568e36ec17dc9dd761abbe"}, + {file = "mmh3-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9f069b5421b6d2ac24dd1928788f986cb5393baeca8d758838946e3546a1a723"}, + {file = "mmh3-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:363a9a4342aa93d524b01336a646da4b20fb52b22b883d7a4a173f4b4e015451"}, + {file = "mmh3-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:453fff6505db5f3c2e2ee562744b8de49a58f83a609f178a1d3238036fbb4a72"}, + {file = "mmh3-4.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8397bb38314ba93f9c87e7feb724d093a3b3df566dccab9d088e18792c8ad6fd"}, + {file = "mmh3-4.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:130801c630768b39c0c115b4df1ba649318179a47981cbdd7171c8f78cfe1e4b"}, + {file = "mmh3-4.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ad9a741871550ecb7217449e423806f8e6e76c6fe90383992c8db15cb55e9d9"}, + {file = "mmh3-4.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07a7447cbfcaa269d8f8c64dd12924fe523f0ac04135428a1f327d769fa9b1a4"}, + {file = "mmh3-4.0.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95d03f341ce11ee9f325af910bc588dda519f4b15b60abab72ea286a6aad572b"}, + {file = "mmh3-4.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2cff9758d6b0f7f00d805e9d1d019e8b7f4da4b56795843e0339633fd67bc3a4"}, + {file = "mmh3-4.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:28c182fcb472bb102bb6f8f92bcc268be469220e8ba6dca383860e494a91671f"}, + {file = "mmh3-4.0.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:b52775bffc0463e32cddaea76afd61a992b15091fe1955418b44cb7b87a7aea9"}, + {file = "mmh3-4.0.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ec8ba7a8357a33af6c41a29a3015f796643e998a1a33d95cb773efc98ae669d2"}, + {file = "mmh3-4.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:129f2c948ce8b884ddcad0f755a8b3245302eb06c3de1b4aa9a45b643c63ca04"}, + {file = "mmh3-4.0.0-cp39-cp39-win32.whl", hash = "sha256:a373025a487295c9e6cbf86159665f49963077d39a843707519c678f7e6791a2"}, + {file = "mmh3-4.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:5790d3bf330a6be8bea19fcc03965a5cf6c9a37021ecf9202e54ed7c3d33bd23"}, + {file = "mmh3-4.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:2a2db279f0c97619a6d3cc291168cc47a3cbd73cf1767a058e5cc765252260a1"}, + {file = "mmh3-4.0.0.tar.gz", hash = "sha256:056b83d04e595547d0407cc8e5aa5d8ba8802a8afa417b64c1c30235b5389e30"}, ] +[package.extras] +test = ["mypy (>=1.0)", "pytest (>=7.0.0)"] + [[package]] -name = "mypy" -version = "0.981" -description = "Optional static typing for Python" -category = "dev" +name = "multidict" +version = "6.0.4" +description = "multidict implementation" optional = false python-versions = ">=3.7" files = [ - {file = "mypy-0.981-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4bc460e43b7785f78862dab78674e62ec3cd523485baecfdf81a555ed29ecfa0"}, - {file = "mypy-0.981-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:756fad8b263b3ba39e4e204ee53042671b660c36c9017412b43af210ddee7b08"}, - {file = "mypy-0.981-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16a0145d6d7d00fbede2da3a3096dcc9ecea091adfa8da48fa6a7b75d35562d"}, - {file = "mypy-0.981-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce65f70b14a21fdac84c294cde75e6dbdabbcff22975335e20827b3b94bdbf49"}, - {file = "mypy-0.981-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e35d764784b42c3e256848fb8ed1d4292c9fc0098413adb28d84974c095b279"}, - {file = "mypy-0.981-cp310-cp310-win_amd64.whl", hash = "sha256:e53773073c864d5f5cec7f3fc72fbbcef65410cde8cc18d4f7242dea60dac52e"}, - {file = "mypy-0.981-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ee196b1d10b8b215e835f438e06965d7a480f6fe016eddbc285f13955cca659"}, - {file = "mypy-0.981-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ad21d4c9d3673726cf986ea1d0c9fb66905258709550ddf7944c8f885f208be"}, - {file = "mypy-0.981-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d1debb09043e1f5ee845fa1e96d180e89115b30e47c5d3ce53bc967bab53f62d"}, - {file = "mypy-0.981-cp37-cp37m-win_amd64.whl", hash = "sha256:9f362470a3480165c4c6151786b5379351b790d56952005be18bdbdd4c7ce0ae"}, - {file = "mypy-0.981-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c9e0efb95ed6ca1654951bd5ec2f3fa91b295d78bf6527e026529d4aaa1e0c30"}, - {file = "mypy-0.981-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e178eaffc3c5cd211a87965c8c0df6da91ed7d258b5fc72b8e047c3771317ddb"}, - {file = "mypy-0.981-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:06e1eac8d99bd404ed8dd34ca29673c4346e76dd8e612ea507763dccd7e13c7a"}, - {file = "mypy-0.981-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa38f82f53e1e7beb45557ff167c177802ba7b387ad017eab1663d567017c8ee"}, - {file = "mypy-0.981-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:64e1f6af81c003f85f0dfed52db632817dabb51b65c0318ffbf5ff51995bbb08"}, - {file = "mypy-0.981-cp38-cp38-win_amd64.whl", hash = "sha256:e1acf62a8c4f7c092462c738aa2c2489e275ed386320c10b2e9bff31f6f7e8d6"}, - {file = "mypy-0.981-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b6ede64e52257931315826fdbfc6ea878d89a965580d1a65638ef77cb551f56d"}, - {file = "mypy-0.981-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eb3978b191b9fa0488524bb4ffedf2c573340e8c2b4206fc191d44c7093abfb7"}, - {file = "mypy-0.981-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f8fcf7b4b3cc0c74fb33ae54a4cd00bb854d65645c48beccf65fa10b17882c"}, - {file = "mypy-0.981-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64d2ce043a209a297df322eb4054dfbaa9de9e8738291706eaafda81ab2b362"}, - {file = "mypy-0.981-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2ee3dbc53d4df7e6e3b1c68ac6a971d3a4fb2852bf10a05fda228721dd44fae1"}, - {file = "mypy-0.981-cp39-cp39-win_amd64.whl", hash = "sha256:8e8e49aa9cc23aa4c926dc200ce32959d3501c4905147a66ce032f05cb5ecb92"}, - {file = "mypy-0.981-py3-none-any.whl", hash = "sha256:794f385653e2b749387a42afb1e14c2135e18daeb027e0d97162e4b7031210f8"}, - {file = "mypy-0.981.tar.gz", hash = "sha256:ad77c13037d3402fbeffda07d51e3f228ba078d1c7096a73759c9419ea031bf4"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, ] -[package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" +[[package]] +name = "mypy" +version = "1.4.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, + {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, + {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, + {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, + {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, + {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, + {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, + {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, + {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, + {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, + {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, + {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, + {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, + {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, + {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, + {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, + {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, + {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, + {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.4" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "main" -optional = false -python-versions = ">=2.7" -files = [ - {file = "mypy_extensions-0.4.4.tar.gz", hash = "sha256:c8b707883a96efe9b4bb3aaf0dcc07e7e217d7d8368eec4db4049ee9e142f4fd"}, -] - -[[package]] -name = "numpy" -version = "1.25.2" -description = "Fundamental package for array computing in Python" -category = "main" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, - {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, - {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, - {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, - {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, - {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, - {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, - {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, - {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, - {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, - {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, - {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "oauth2client" version = "4.1.3" description = "OAuth 2.0 client library" -category = "main" optional = false python-versions = "*" files = [ @@ -1775,7 +2100,6 @@ six = ">=1.6.1" name = "ordered-set" version = "4.1.0" description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1790,7 +2114,6 @@ dev = ["black", "mypy", "pytest"] name = "outcome" version = "1.2.0" description = "Capture the outcome of Python function calls." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1805,7 +2128,6 @@ attrs = ">=19.2.0" name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1817,7 +2139,6 @@ files = [ name = "pandocfilters" version = "1.5.0" description = "Utilities for writing pandoc filters in python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1827,90 +2148,76 @@ files = [ [[package]] name = "pathspec" -version = "0.11.2" +version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, ] [[package]] name = "pillow" -version = "9.5.0" +version = "10.0.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, - {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, - {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, - {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, - {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, - {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, - {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, - {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, - {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, - {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, - {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, - {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, - {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, - {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, - {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, - {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, - {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, + {file = "Pillow-10.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891"}, + {file = "Pillow-10.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf"}, + {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3"}, + {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992"}, + {file = "Pillow-10.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de"}, + {file = "Pillow-10.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485"}, + {file = "Pillow-10.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629"}, + {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"}, + {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"}, + {file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"}, + {file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"}, + {file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff"}, + {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"}, + {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"}, + {file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"}, + {file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"}, + {file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51"}, + {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86"}, + {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7"}, + {file = "Pillow-10.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0"}, + {file = "Pillow-10.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa"}, + {file = "Pillow-10.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba"}, + {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3"}, + {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017"}, + {file = "Pillow-10.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3"}, + {file = "Pillow-10.0.0.tar.gz", hash = "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396"}, ] [package.extras] @@ -1919,25 +2226,37 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "platformdirs" -version = "3.10.0" +version = "3.8.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, + {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.38" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"}, + {file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"}, +] + +[package.dependencies] +wcwidth = "*" [[package]] name = "psycogreen" version = "1.0.2" description = "psycopg2 integration with coroutine libraries" -category = "main" optional = false python-versions = "*" files = [ @@ -1948,7 +2267,6 @@ files = [ name = "psycopg2-binary" version = "2.9.6" description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2018,24 +2336,25 @@ files = [ [[package]] name = "pyaml" -version = "21.10.1" -description = "PyYAML-based module to produce pretty and readable YAML-serialized data" -category = "main" +version = "23.5.9" +description = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"}, - {file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"}, + {file = "pyaml-23.5.9-py3-none-any.whl", hash = "sha256:b7fa20b43c5b6e5c8b7406a2408fe533efd65a6459feff828f918342f043ef4c"}, + {file = "pyaml-23.5.9.tar.gz", hash = "sha256:4c4b28b6fe89336000f08646f3cf1f6b68fb11e4c409626b77562e65a577273b"}, ] [package.dependencies] PyYAML = "*" +[package.extras] +anchors = ["unidecode"] + [[package]] name = "pyasn1" version = "0.5.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -2047,7 +2366,6 @@ files = [ name = "pyasn1-modules" version = "0.3.0" description = "A collection of ASN.1-based protocols modules" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -2060,21 +2378,19 @@ pyasn1 = ">=0.4.6,<0.6.0" [[package]] name = "pycodestyle" -version = "2.11.0" +version = "2.10.0" description = "Python style guide checker" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.6" files = [ - {file = "pycodestyle-2.11.0-py2.py3-none-any.whl", hash = "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8"}, - {file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"}, + {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, + {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, ] [[package]] name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2086,7 +2402,6 @@ files = [ name = "pygments" version = "2.15.1" description = "Pygments is a syntax highlighting package written in Python." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2101,7 +2416,6 @@ plugins = ["importlib-metadata"] name = "pylatex" version = "1.4.1" description = "A Python library for creating LaTeX files and snippets" -category = "main" optional = false python-versions = "*" files = [ @@ -2124,7 +2438,6 @@ testing = ["coverage", "flake8 (<3.0.0)", "flake8-putty", "flake8_docstrings (== name = "pyopenssl" version = "23.2.0" description = "Python wrapper module around the OpenSSL library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2143,7 +2456,6 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] name = "pypandoc" version = "1.11" description = "Thin wrapper for pandoc." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2153,14 +2465,13 @@ files = [ [[package]] name = "pyparsing" -version = "3.1.1" +version = "3.1.0" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, - {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, + {file = "pyparsing-3.1.0-py3-none-any.whl", hash = "sha256:d554a96d1a7d3ddaf7183104485bc19fd80543ad6ac5bdb6426719d766fb06c1"}, + {file = "pyparsing-3.1.0.tar.gz", hash = "sha256:edb662d6fe322d6e990b1594b5feaeadf806803359e3d4d42f11e295e588f0ea"}, ] [package.extras] @@ -2170,7 +2481,6 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pysaml2" version = "7.4.2" description = "Python implementation of SAML Version 2 Standard" -category = "main" optional = false python-versions = ">=3.9,<4.0" files = [ @@ -2194,7 +2504,6 @@ s2repoze = ["paste", "repoze.who", "zope.interface"] name = "pysocks" version = "1.7.1" description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2207,7 +2516,6 @@ files = [ name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -2220,21 +2528,19 @@ six = ">=1.5" [[package]] name = "python-gnupg" -version = "0.5.1" +version = "0.5.0" description = "A wrapper for the Gnu Privacy Guard (GPG or GnuPG)" -category = "main" optional = false python-versions = "*" files = [ - {file = "python-gnupg-0.5.1.tar.gz", hash = "sha256:5674bad4e93876c0b0d3197e314d7f942d39018bf31e2b833f6788a6813c3fb8"}, - {file = "python_gnupg-0.5.1-py2.py3-none-any.whl", hash = "sha256:bf9b2d9032ef38139b7d64184176cd0b293eaeae6e4f93f50e304c7051174482"}, + {file = "python-gnupg-0.5.0.tar.gz", hash = "sha256:70758e387fc0e0c4badbcb394f61acbe68b34970a8fed7e0f7c89469fe17912a"}, + {file = "python_gnupg-0.5.0-py2.py3-none-any.whl", hash = "sha256:345723a03e67b82aba0ea8ae2328b2e4a3906fbe2c18c4082285c3b01068f270"}, ] [[package]] name = "python-magic" version = "0.4.27" description = "File type identification using libmagic" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2246,7 +2552,6 @@ files = [ name = "python3-openid" version = "3.2.0" description = "OpenID support for modern servers and consumers." -category = "main" optional = false python-versions = "*" files = [ @@ -2263,111 +2568,68 @@ postgresql = ["psycopg2"] [[package]] name = "pytz" -version = "2022.7.1" +version = "2023.3" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ - {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, - {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, + {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, + {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, ] [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "qulacs" -version = "0.6.1" -description = "Quantum circuit simulator for research" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "qulacs-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1f1951e9cb55e11ded47b7a652bb03b06f986ba19e6c2ea76b40b159f297f62"}, - {file = "qulacs-0.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b922972a7b9a827c16dc9e2c14478f276ead2eacad7d545db03ab795854b94f"}, - {file = "qulacs-0.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6892d39d46e1b5bd89def6cb96e67316abcb04ba1e1a5d894a2d7a155c0eb27"}, - {file = "qulacs-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:df0f0d0fe6f53ea628960ab32239ba0dd92a5e4b88a2936a24442fd64e58e515"}, - {file = "qulacs-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fea830bebadd34f07fb35680c11e7cd712343ea8dbead0d5ef417c70605a4ea4"}, - {file = "qulacs-0.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4807a134e00a9d6f20c488036933ad7c570ba07bcb813c8afd2b06f60c8c98a1"}, - {file = "qulacs-0.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ea1c78fda2830a52702cffb7f22b3900282329ac5dcfa7c947dd00b0b126ba5"}, - {file = "qulacs-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc18d30a7e809786df0007a7bd7a918f53cc60369ae19a671cb55b65ea5056e5"}, - {file = "qulacs-0.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3f29acc7b86913612ba2b39241b365e0dea9925a4929998dfc2eb94e7d43b81e"}, - {file = "qulacs-0.6.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69f9ce7c836e0b755a158a4cc392b49ea414c6eb37b109ec3149d7b1f13005dc"}, - {file = "qulacs-0.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:536212f0742efa49a6fc91c4c3349b48e669d47f7881a90f88c491d19ef7e732"}, - {file = "qulacs-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:17142f904cb62f688f76feee07742072bfb9817992dbe5fa73da667e04425c55"}, - {file = "qulacs-0.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c50733917f1b01b11e6be177255fab1be7af5563984f9c06827d038b4af4585"}, - {file = "qulacs-0.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bd699a68034a2a29e153d30735b561f57f0796bb4d30b2576d1cce1671e8d0b"}, - {file = "qulacs-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:7e251102e157f36ade2b16ff34d65d55515f88ca0bf5be0a93f50232fd08eb97"}, - {file = "qulacs-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b553c82db86e9e972b7cfdb816276123cc15aa4877375e8be14c48ec381109aa"}, - {file = "qulacs-0.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:60ed40efbeed8141ca171fceefbfa581d93a961601e4053b731262900724e383"}, - {file = "qulacs-0.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06aaf7a7a01e6179a0362e170c923ac5834b8d0c5433a677dbcfed9aa9fdff02"}, - {file = "qulacs-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:331a8084ed08b0e356149e51a756afd6c628fceb070d64c500d2c657351de909"}, - {file = "qulacs-0.6.1.tar.gz", hash = "sha256:f958f26de98a37c519ff092167d440931a3ee77f8f628e2ab7441fe15322c28a"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] -[package.dependencies] -numpy = "*" -scipy = "*" - -[package.extras] -ci = ["black", "flake8", "isort", "mypy", "openfermion", "pybind11-stubgen", "pytest"] -dev = ["black", "flake8", "isort", "mypy", "openfermion", "pybind11-stubgen", "pytest"] -doc = ["breathe (>=4.33.0,<4.34.0)", "exhale (>=0.3.0,<0.4.0)", "ipykernel (>=6.17.0,<6.18.0)", "myst-parser (>=0.18.0,<0.19.0)", "nbsphinx (>=0.8.0,<0.9.0)", "sphinx (==4.5.0)", "sphinx-autoapi (>=2.0.0,<2.1.0)", "sphinx-copybutton (>=0.5.0,<0.6.0)", "sphinx-rtd-theme (>=1.0.0,<1.1.0)"] -test = ["openfermion"] - [[package]] name = "recommonmark" version = "0.7.1" description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." -category = "main" optional = false python-versions = "*" files = [ @@ -2384,7 +2646,6 @@ sphinx = ">=1.3.1" name = "redis" version = "4.6.0" description = "Python client for Redis database and key-value store" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2403,7 +2664,6 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2423,28 +2683,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "responses" -version = "0.21.0" +version = "0.23.1" description = "A utility library for mocking out the `requests` Python library." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "responses-0.21.0-py3-none-any.whl", hash = "sha256:2dcc863ba63963c0c3d9ee3fa9507cbe36b7d7b0fccb4f0bdfd9e96c539b1487"}, - {file = "responses-0.21.0.tar.gz", hash = "sha256:b82502eb5f09a0289d8e209e7bad71ef3978334f56d09b444253d5ad67bf5253"}, + {file = "responses-0.23.1-py3-none-any.whl", hash = "sha256:8a3a5915713483bf353b6f4079ba8b2a29029d1d1090a503c70b0dc5d9d0c7bd"}, + {file = "responses-0.23.1.tar.gz", hash = "sha256:c4d9aa9fc888188f0c673eff79a8dadbe2e75b7fe879dc80a221a06e0a68138f"}, ] [package.dependencies] -requests = ">=2.0,<3.0" +pyyaml = "*" +requests = ">=2.22.0,<3.0" +types-PyYAML = "*" urllib3 = ">=1.25.10" [package.extras] -tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-localserver", "types-mock", "types-requests"] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] [[package]] name = "rsa" version = "4.9" description = "Pure-Python RSA implementation" -category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -2455,55 +2715,15 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" -[[package]] -name = "scipy" -version = "1.9.3" -description = "Fundamental algorithms for scientific computing in Python" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, - {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, - {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, - {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, - {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, - {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, - {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, -] - -[package.dependencies] -numpy = ">=1.18.5,<1.26.0" - -[package.extras] -dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] -doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] -test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - [[package]] name = "selenium" -version = "4.11.2" +version = "4.10.0" description = "" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "selenium-4.11.2-py3-none-any.whl", hash = "sha256:98e72117b194b3fa9c69b48998f44bf7dd4152c7bd98544911a1753b9f03cc7d"}, - {file = "selenium-4.11.2.tar.gz", hash = "sha256:9f9a5ed586280a3594f7461eb1d9dab3eac9d91e28572f365e9b98d9d03e02b5"}, + {file = "selenium-4.10.0-py3-none-any.whl", hash = "sha256:40241b9d872f58959e9b34e258488bf11844cd86142fd68182bd41db9991fc5c"}, + {file = "selenium-4.10.0.tar.gz", hash = "sha256:871bf800c4934f745b909c8dfc7d15c65cf45bd2e943abd54451c810ada395e3"}, ] [package.dependencies] @@ -2514,26 +2734,24 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]} [[package]] name = "setuptools" -version = "62.6.0" +version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-62.6.0-py3-none-any.whl", hash = "sha256:c1848f654aea2e3526d17fc3ce6aeaa5e7e24e66e645b5be2171f3f6b4e5a178"}, - {file = "setuptools-62.6.0.tar.gz", hash = "sha256:990a4f7861b31532871ab72331e755b5f14efbe52d336ea7f6118144dd478741"}, + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "simplejson" version = "3.19.1" description = "Simple, fast, extensible JSON encoder/decoder for Python" -category = "main" optional = false python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2628,7 +2846,6 @@ files = [ name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2640,7 +2857,6 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2652,7 +2868,6 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "main" optional = false python-versions = "*" files = [ @@ -2664,7 +2879,6 @@ files = [ name = "sortedcontainers" version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -category = "main" optional = false python-versions = "*" files = [ @@ -2676,7 +2890,6 @@ files = [ name = "soupsieve" version = "2.4.1" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2686,26 +2899,25 @@ files = [ [[package]] name = "sphinx" -version = "5.3.0" +version = "7.0.1" description = "Python documentation generator" -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, - {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, + {file = "Sphinx-7.0.1.tar.gz", hash = "sha256:61e025f788c5977d9412587e733733a289e2b9fdc2fef8868ddfbfc4ccfe881d"}, + {file = "sphinx-7.0.1-py3-none-any.whl", hash = "sha256:60c5e04756c1709a98845ed27a2eed7a556af3993afb66e77fec48189f742616"}, ] [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.20" +docutils = ">=0.18.1,<0.21" imagesize = ">=1.3" Jinja2 = ">=3.0" packaging = ">=21.0" -Pygments = ">=2.12" -requests = ">=2.5.0" +Pygments = ">=2.13" +requests = ">=2.25.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" @@ -2716,14 +2928,13 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] +test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] [[package]] name = "sphinxcontrib-applehelp" version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2739,7 +2950,6 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2755,7 +2965,6 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2771,7 +2980,6 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2786,7 +2994,6 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2802,7 +3009,6 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2816,70 +3022,87 @@ test = ["pytest"] [[package]] name = "sqlalchemy" -version = "1.3.24" +version = "1.4.48" description = "Database Abstraction Library" -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"}, - {file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"}, - {file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"}, - {file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"}, - {file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"}, - {file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"}, - {file = "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"}, - {file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"}, - {file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"}, -] + {file = "SQLAlchemy-1.4.48-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:4bac3aa3c3d8bc7408097e6fe8bf983caa6e9491c5d2e2488cfcfd8106f13b6a"}, + {file = "SQLAlchemy-1.4.48-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dbcae0e528d755f4522cad5842f0942e54b578d79f21a692c44d91352ea6d64e"}, + {file = "SQLAlchemy-1.4.48-cp27-cp27m-win32.whl", hash = "sha256:cbbe8b8bffb199b225d2fe3804421b7b43a0d49983f81dc654d0431d2f855543"}, + {file = "SQLAlchemy-1.4.48-cp27-cp27m-win_amd64.whl", hash = "sha256:627e04a5d54bd50628fc8734d5fc6df2a1aa5962f219c44aad50b00a6cdcf965"}, + {file = "SQLAlchemy-1.4.48-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9af1db7a287ef86e0f5cd990b38da6bd9328de739d17e8864f1817710da2d217"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ce7915eecc9c14a93b73f4e1c9d779ca43e955b43ddf1e21df154184f39748e5"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5381ddd09a99638f429f4cbe1b71b025bed318f6a7b23e11d65f3eed5e181c33"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:87609f6d4e81a941a17e61a4c19fee57f795e96f834c4f0a30cee725fc3f81d9"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb0808ad34167f394fea21bd4587fc62f3bd81bba232a1e7fbdfa17e6cfa7cd7"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-win32.whl", hash = "sha256:d53cd8bc582da5c1c8c86b6acc4ef42e20985c57d0ebc906445989df566c5603"}, + {file = "SQLAlchemy-1.4.48-cp310-cp310-win_amd64.whl", hash = "sha256:4355e5915844afdc5cf22ec29fba1010166e35dd94a21305f49020022167556b"}, + {file = "SQLAlchemy-1.4.48-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:066c2b0413e8cb980e6d46bf9d35ca83be81c20af688fedaef01450b06e4aa5e"}, + {file = "SQLAlchemy-1.4.48-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c99bf13e07140601d111a7c6f1fc1519914dd4e5228315bbda255e08412f61a4"}, + {file = "SQLAlchemy-1.4.48-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee26276f12614d47cc07bc85490a70f559cba965fb178b1c45d46ffa8d73fda"}, + {file = "SQLAlchemy-1.4.48-cp311-cp311-win32.whl", hash = "sha256:49c312bcff4728bffc6fb5e5318b8020ed5c8b958a06800f91859fe9633ca20e"}, + {file = "SQLAlchemy-1.4.48-cp311-cp311-win_amd64.whl", hash = "sha256:cef2e2abc06eab187a533ec3e1067a71d7bbec69e582401afdf6d8cad4ba3515"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3509159e050bd6d24189ec7af373359f07aed690db91909c131e5068176c5a5d"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc2ab4d9f6d9218a5caa4121bdcf1125303482a1cdcfcdbd8567be8518969c0"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1ddbbcef9bcedaa370c03771ebec7e39e3944782bef49e69430383c376a250b"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f82d8efea1ca92b24f51d3aea1a82897ed2409868a0af04247c8c1e4fef5890"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-win32.whl", hash = "sha256:e3e98d4907805b07743b583a99ecc58bf8807ecb6985576d82d5e8ae103b5272"}, + {file = "SQLAlchemy-1.4.48-cp36-cp36m-win_amd64.whl", hash = "sha256:25887b4f716e085a1c5162f130b852f84e18d2633942c8ca40dfb8519367c14f"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0817c181271b0ce5df1aa20949f0a9e2426830fed5ecdcc8db449618f12c2730"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe1dd2562313dd9fe1778ed56739ad5d9aae10f9f43d9f4cf81d65b0c85168bb"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:68413aead943883b341b2b77acd7a7fe2377c34d82e64d1840860247cec7ff7c"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbde5642104ac6e95f96e8ad6d18d9382aa20672008cf26068fe36f3004491df"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-win32.whl", hash = "sha256:11c6b1de720f816c22d6ad3bbfa2f026f89c7b78a5c4ffafb220e0183956a92a"}, + {file = "SQLAlchemy-1.4.48-cp37-cp37m-win_amd64.whl", hash = "sha256:eb5464ee8d4bb6549d368b578e9529d3c43265007193597ddca71c1bae6174e6"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:92e6133cf337c42bfee03ca08c62ba0f2d9695618c8abc14a564f47503157be9"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d29a3fc6d9c45962476b470a81983dd8add6ad26fdbfae6d463b509d5adcda"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:005e942b451cad5285015481ae4e557ff4154dde327840ba91b9ac379be3b6ce"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8cfe951ed074ba5e708ed29c45397a95c4143255b0d022c7c8331a75ae61f3"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-win32.whl", hash = "sha256:2b9af65cc58726129d8414fc1a1a650dcdd594ba12e9c97909f1f57d48e393d3"}, + {file = "SQLAlchemy-1.4.48-cp38-cp38-win_amd64.whl", hash = "sha256:2b562e9d1e59be7833edf28b0968f156683d57cabd2137d8121806f38a9d58f4"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:a1fc046756cf2a37d7277c93278566ddf8be135c6a58397b4c940abf837011f4"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d9b55252d2ca42a09bcd10a697fa041e696def9dfab0b78c0aaea1485551a08"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6dab89874e72a9ab5462997846d4c760cdb957958be27b03b49cf0de5e5c327c"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd8b5ee5a3acc4371f820934b36f8109ce604ee73cc668c724abb054cebcb6e"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-win32.whl", hash = "sha256:eee09350fd538e29cfe3a496ec6f148504d2da40dbf52adefb0d2f8e4d38ccc4"}, + {file = "SQLAlchemy-1.4.48-cp39-cp39-win_amd64.whl", hash = "sha256:7ad2b0f6520ed5038e795cc2852eb5c1f20fa6831d73301ced4aafbe3a10e1f6"}, + {file = "SQLAlchemy-1.4.48.tar.gz", hash = "sha256:b47bc287096d989a0838ce96f7d8e966914a24da877ed41a7531d44b55cdb8df"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"} [package.extras] +aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] mssql-pyodbc = ["pyodbc"] -mysql = ["mysqlclient"] -oracle = ["cx-oracle"] -postgresql = ["psycopg2"] -postgresql-pg8000 = ["pg8000 (<1.16.6)"] +mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] +mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] pymysql = ["pymysql", "pymysql (<1)"] +sqlcipher = ["sqlcipher3-binary"] [[package]] name = "sqlalchemy-utils" -version = "0.38.3" +version = "0.41.1" description = "Various utility functions for SQLAlchemy." -category = "main" optional = false -python-versions = "~=3.6" +python-versions = ">=3.6" files = [ - {file = "SQLAlchemy-Utils-0.38.3.tar.gz", hash = "sha256:9f9afba607a40455cf703adfa9846584bf26168a0c5a60a70063b70d65051f4d"}, - {file = "SQLAlchemy_Utils-0.38.3-py3-none-any.whl", hash = "sha256:5c13b5d08adfaa85f3d4e8ec09a75136216fad41346980d02974a70a77988bf9"}, + {file = "SQLAlchemy-Utils-0.41.1.tar.gz", hash = "sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74"}, + {file = "SQLAlchemy_Utils-0.41.1-py3-none-any.whl", hash = "sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801"}, ] [package.dependencies] @@ -2894,51 +3117,25 @@ intervals = ["intervals (>=0.7.1)"] password = ["passlib (>=1.6,<2.0)"] pendulum = ["pendulum (>=2.0.5)"] phone = ["phonenumbers (>=5.9.2)"] -test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] -test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] timezone = ["python-dateutil"] url = ["furl (>=0.4.1)"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - [[package]] name = "trio" -version = "0.22.2" +version = "0.22.1" description = "A friendly Python library for async concurrency and I/O" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "trio-0.22.2-py3-none-any.whl", hash = "sha256:f43da357620e5872b3d940a2e3589aa251fd3f881b65a608d742e00809b1ec38"}, - {file = "trio-0.22.2.tar.gz", hash = "sha256:3887cf18c8bcc894433420305468388dac76932e9668afa1c49aa3806b6accb3"}, + {file = "trio-0.22.1-py3-none-any.whl", hash = "sha256:1270da4a4a33caf33f85c6a255f2ef5f71629a3ec9aea31a052062b673ae58d3"}, + {file = "trio-0.22.1.tar.gz", hash = "sha256:eb5f641b313eb502a16de176d84cecd9ccf2394a7c8655d2297398376bb15eca"}, ] [package.dependencies] attrs = ">=20.1.0" cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} -exceptiongroup = {version = ">=1.0.0rc9", markers = "python_version < \"3.11\""} idna = "*" outcome = "*" sniffio = "*" @@ -2948,7 +3145,6 @@ sortedcontainers = "*" name = "trio-websocket" version = "0.10.3" description = "WebSocket library for Trio" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2965,7 +3161,6 @@ wsproto = ">=0.14" name = "types-attrs" version = "19.1.0" description = "Typing stubs for attrs" -category = "dev" optional = false python-versions = "*" files = [ @@ -2974,21 +3169,19 @@ files = [ [[package]] name = "types-bleach" -version = "5.0.3.1" +version = "6.0.0.3" description = "Typing stubs for bleach" -category = "dev" optional = false python-versions = "*" files = [ - {file = "types-bleach-5.0.3.1.tar.gz", hash = "sha256:ce8772ea5126dab1883851b41e3aeff229aa5213ced36096990344e632e92373"}, - {file = "types_bleach-5.0.3.1-py3-none-any.whl", hash = "sha256:af5f1b3a54ff279f54c29eccb2e6988ebb6718bc4061469588a5fd4880a79287"}, + {file = "types-bleach-6.0.0.3.tar.gz", hash = "sha256:8ce7896d4f658c562768674ffcf07492c7730e128018f03edd163ff912bfadee"}, + {file = "types_bleach-6.0.0.3-py3-none-any.whl", hash = "sha256:d43eaf30a643ca824e16e2dcdb0c87ef9226237e2fa3ac4732a50cb3f32e145f"}, ] [[package]] name = "types-filelock" version = "3.2.7" description = "Typing stubs for filelock" -category = "dev" optional = false python-versions = "*" files = [ @@ -2998,14 +3191,13 @@ files = [ [[package]] name = "types-pyopenssl" -version = "23.2.0.2" +version = "23.2.0.1" description = "Typing stubs for pyOpenSSL" -category = "dev" optional = false python-versions = "*" files = [ - {file = "types-pyOpenSSL-23.2.0.2.tar.gz", hash = "sha256:6a010dac9ecd42b582d7dd2cc3e9e40486b79b3b64bb2fffba1474ff96af906d"}, - {file = "types_pyOpenSSL-23.2.0.2-py3-none-any.whl", hash = "sha256:19536aa3debfbe25a918cf0d898e9f5fbbe6f3594a429da7914bf331deb1b342"}, + {file = "types-pyOpenSSL-23.2.0.1.tar.gz", hash = "sha256:beeb5d22704c625a1e4b6dc756355c5b4af0b980138b702a9d9f932acf020903"}, + {file = "types_pyOpenSSL-23.2.0.1-py3-none-any.whl", hash = "sha256:0568553f104466f1b8e0db3360fbe6770137d02e21a1a45c209bf2b1b03d90d4"}, ] [package.dependencies] @@ -3015,7 +3207,6 @@ cryptography = ">=35.0.0" name = "types-pysaml2" version = "1.0.1" description = "Type Stubs for pysaml2" -category = "dev" optional = false python-versions = ">=3.7, <4" files = [ @@ -3028,50 +3219,46 @@ dev = ["mypy (==0.991)", "pipenv-setup (==3.2.0)", "pysaml2 (==7.2.1)", "twine ( [[package]] name = "types-python-dateutil" -version = "2.8.19.14" +version = "2.8.19.13" description = "Typing stubs for python-dateutil" -category = "dev" optional = false python-versions = "*" files = [ - {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, - {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, + {file = "types-python-dateutil-2.8.19.13.tar.gz", hash = "sha256:09a0275f95ee31ce68196710ed2c3d1b9dc42e0b61cc43acc369a42cb939134f"}, + {file = "types_python_dateutil-2.8.19.13-py3-none-any.whl", hash = "sha256:0b0e7c68e7043b0354b26a1e0225cb1baea7abb1b324d02b50e2d08f1221043f"}, ] [[package]] name = "types-pytz" -version = "2021.3.8" +version = "2023.3.0.0" description = "Typing stubs for pytz" -category = "dev" optional = false python-versions = "*" files = [ - {file = "types-pytz-2021.3.8.tar.gz", hash = "sha256:41253a3a2bf028b6a3f17b58749a692d955af0f74e975de94f6f4d2d3cd01dbd"}, - {file = "types_pytz-2021.3.8-py3-none-any.whl", hash = "sha256:aef4a917ab28c585d3f474bfce4f4b44b91e95d9d47d4de29dd845e0db8e3910"}, + {file = "types-pytz-2023.3.0.0.tar.gz", hash = "sha256:ecdc70d543aaf3616a7e48631543a884f74205f284cefd6649ddf44c6a820aac"}, + {file = "types_pytz-2023.3.0.0-py3-none-any.whl", hash = "sha256:4fc2a7fbbc315f0b6630e0b899fd6c743705abe1094d007b0e612d10da15e0f3"}, ] [[package]] name = "types-pyyaml" -version = "6.0.12.11" +version = "6.0.12.10" description = "Typing stubs for PyYAML" -category = "dev" optional = false python-versions = "*" files = [ - {file = "types-PyYAML-6.0.12.11.tar.gz", hash = "sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b"}, - {file = "types_PyYAML-6.0.12.11-py3-none-any.whl", hash = "sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d"}, + {file = "types-PyYAML-6.0.12.10.tar.gz", hash = "sha256:ebab3d0700b946553724ae6ca636ea932c1b0868701d4af121630e78d695fc97"}, + {file = "types_PyYAML-6.0.12.10-py3-none-any.whl", hash = "sha256:662fa444963eff9b68120d70cda1af5a5f2aa57900003c2006d7626450eaae5f"}, ] [[package]] name = "types-redis" -version = "4.6.0.3" +version = "4.6.0.1" description = "Typing stubs for redis" -category = "dev" optional = false python-versions = "*" files = [ - {file = "types-redis-4.6.0.3.tar.gz", hash = "sha256:efdef37dc0c04bf5786195651fd694f8bfdd693eac09ec4af46d90f72652558f"}, - {file = "types_redis-4.6.0.3-py3-none-any.whl", hash = "sha256:67c44c14369c33c2a300da2a50b5607c0fc888f7b85eeb7c73e15c78a0f05edd"}, + {file = "types-redis-4.6.0.1.tar.gz", hash = "sha256:1254d525de7a45e2efaacb6969e67ad1dd5cc359a092022200583a3f04868669"}, + {file = "types_redis-4.6.0.1-py3-none-any.whl", hash = "sha256:88ceb79c27f2084ad6f0b8514f8fcd8a740811f07c25f3fef5c9e843fc6c60a2"}, ] [package.dependencies] @@ -3080,14 +3267,13 @@ types-pyOpenSSL = "*" [[package]] name = "types-requests" -version = "2.31.0.2" +version = "2.31.0.1" description = "Typing stubs for requests" -category = "dev" optional = false python-versions = "*" files = [ - {file = "types-requests-2.31.0.2.tar.gz", hash = "sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40"}, - {file = "types_requests-2.31.0.2-py3-none-any.whl", hash = "sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a"}, + {file = "types-requests-2.31.0.1.tar.gz", hash = "sha256:3de667cffa123ce698591de0ad7db034a5317457a596eb0b4944e5a9d9e8d1ac"}, + {file = "types_requests-2.31.0.1-py3-none-any.whl", hash = "sha256:afb06ef8f25ba83d59a1d424bd7a5a939082f94b94e90ab5e6116bd2559deaa3"}, ] [package.dependencies] @@ -3095,21 +3281,19 @@ types-urllib3 = "*" [[package]] name = "types-urllib3" -version = "1.26.25.14" +version = "1.26.25.13" description = "Typing stubs for urllib3" -category = "dev" optional = false python-versions = "*" files = [ - {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, - {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, + {file = "types-urllib3-1.26.25.13.tar.gz", hash = "sha256:3300538c9dc11dad32eae4827ac313f5d986b8b21494801f1bf97a1ac6c03ae5"}, + {file = "types_urllib3-1.26.25.13-py3-none-any.whl", hash = "sha256:5dbd1d2bef14efee43f5318b5d36d805a489f6600252bb53626d4bfafd95e27c"}, ] [[package]] name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3119,26 +3303,34 @@ files = [ [[package]] name = "typing-inspect" -version = "0.7.1" +version = "0.9.0" description = "Runtime inspection utilities for typing module." -category = "main" optional = false python-versions = "*" files = [ - {file = "typing_inspect-0.7.1-py2-none-any.whl", hash = "sha256:b1f56c0783ef0f25fb064a01be6e5407e54cf4a4bf4f3ba3fe51e0bd6dcea9e5"}, - {file = "typing_inspect-0.7.1-py3-none-any.whl", hash = "sha256:3cd7d4563e997719a710a3bfe7ffb544c6b72069b6812a02e9b414a8fa3aaa6b"}, - {file = "typing_inspect-0.7.1.tar.gz", hash = "sha256:047d4097d9b17f46531bf6f014356111a1b6fb821a24fe7ac909853ca2a782aa"}, + {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, + {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, ] [package.dependencies] mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + [[package]] name = "untokenize" version = "0.1.1" description = "Transforms tokens into original source code (while preserving whitespace)." -category = "main" optional = false python-versions = "*" files = [ @@ -3147,14 +3339,13 @@ files = [ [[package]] name = "urllib3" -version = "2.0.4" +version = "2.0.3" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"}, + {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"}, ] [package.dependencies] @@ -3168,21 +3359,19 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "vine" -version = "1.3.0" +version = "5.0.0" description = "Promises, promises, promises." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" files = [ - {file = "vine-1.3.0-py2.py3-none-any.whl", hash = "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"}, - {file = "vine-1.3.0.tar.gz", hash = "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87"}, + {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"}, + {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"}, ] [[package]] name = "voikko" version = "0.5" description = "Python 3 version of libvoikko and the word inflecting library" -category = "main" optional = false python-versions = ">=3" files = [ @@ -3194,7 +3383,6 @@ files = [ name = "wand" version = "0.6.11" description = "Ctypes-based simple MagickWand API binding for Python" -category = "main" optional = false python-versions = "*" files = [ @@ -3206,11 +3394,21 @@ files = [ doc = ["Sphinx (>=5.3.0)"] test = ["pytest (>=7.2.0)"] +[[package]] +name = "wcwidth" +version = "0.2.6" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] + [[package]] name = "webargs" version = "5.5.0" description = "Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, and aiohttp." -category = "main" optional = false python-versions = "*" files = [ @@ -3234,7 +3432,6 @@ tests = ["Django (>=1.11.16)", "Flask (>=0.12.2)", "aiohttp (>=3.0.0)", "bottle name = "webassets" version = "2.0" description = "Media asset management for Python, with glue code for various web frameworks" -category = "main" optional = false python-versions = "*" files = [] @@ -3250,7 +3447,6 @@ resolved_reference = "17d540ef9e0d7ca53aab06322d0e16fd92b59539" name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" -category = "main" optional = false python-versions = "*" files = [ @@ -3260,42 +3456,39 @@ files = [ [[package]] name = "werkzeug" -version = "2.2.2" +version = "2.3.6" description = "The comprehensive WSGI web application library." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.2.2-py3-none-any.whl", hash = "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5"}, - {file = "Werkzeug-2.2.2.tar.gz", hash = "sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f"}, + {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"}, + {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"}, ] [package.dependencies] MarkupSafe = ">=2.1.1" [package.extras] -watchdog = ["watchdog"] +watchdog = ["watchdog (>=2.3)"] [[package]] name = "wheel" -version = "0.37.1" +version = "0.40.0" description = "A built-package format for Python" -category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" files = [ - {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, - {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"}, + {file = "wheel-0.40.0-py3-none-any.whl", hash = "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247"}, + {file = "wheel-0.40.0.tar.gz", hash = "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873"}, ] [package.extras] -test = ["pytest (>=3.0.0)", "pytest-cov"] +test = ["pytest (>=6.0.0)"] [[package]] name = "wsproto" version = "1.2.0" description = "WebSockets state-machine based protocol implementation" -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -3310,7 +3503,6 @@ h11 = ">=0.9.0,<1" name = "wtforms" version = "3.0.1" description = "Form validation and rendering for Python web development." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3326,29 +3518,114 @@ email = ["email-validator"] [[package]] name = "xmlschema" -version = "2.4.0" +version = "2.3.1" description = "An XML Schema validator and decoder" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "xmlschema-2.4.0-py3-none-any.whl", hash = "sha256:dc87be0caaa61f42649899189aab2fd8e0d567f2cf548433ba7b79278d231a4a"}, - {file = "xmlschema-2.4.0.tar.gz", hash = "sha256:d74cd0c10866ac609e1ef94a5a69b018ad16e39077bc6393408b40c6babee793"}, + {file = "xmlschema-2.3.1-py3-none-any.whl", hash = "sha256:eac0e10957723689ff0691785da4ffee1e95df3a874e685a179047f7bf07f8fb"}, + {file = "xmlschema-2.3.1.tar.gz", hash = "sha256:2eb426c5710833a05610c22c8766713a1b87e9405e3eca0b7c658375bf7ec810"}, ] [package.dependencies] -elementpath = ">=4.1.5,<5.0.0" +elementpath = ">=4.1.2,<5.0.0" [package.extras] -codegen = ["elementpath (>=4.1.5,<5.0.0)", "jinja2"] -dev = ["Sphinx", "coverage", "elementpath (>=4.1.5,<5.0.0)", "flake8", "jinja2", "lxml", "lxml-stubs", "memory-profiler", "mypy", "sphinx-rtd-theme", "tox"] -docs = ["Sphinx", "elementpath (>=4.1.5,<5.0.0)", "jinja2", "sphinx-rtd-theme"] +codegen = ["elementpath (>=4.1.2,<5.0.0)", "jinja2"] +dev = ["Sphinx", "coverage", "elementpath (>=4.1.2,<5.0.0)", "flake8", "jinja2", "lxml", "lxml-stubs", "memory-profiler", "mypy", "sphinx-rtd-theme", "tox"] +docs = ["Sphinx", "elementpath (>=4.1.2,<5.0.0)", "jinja2", "sphinx-rtd-theme"] + +[[package]] +name = "yarl" +version = "1.9.2" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, + {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, + {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, + {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, + {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, + {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, + {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, + {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, + {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, + {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, + {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, + {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, + {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, + {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, + {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, + {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, + {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, + {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, + {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, + {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, + {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, + {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, + {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, + {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, + {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, + {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, + {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" [[package]] name = "zope-event" version = "5.0" description = "Very basic event publishing system" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3367,7 +3644,6 @@ test = ["zope.testrunner"] name = "zope-interface" version = "6.0" description = "Interfaces for Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3413,5 +3689,5 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" -python-versions = "^3.10" -content-hash = "c4cc3021510a9afb169cb497ffe52f3478215a92fea45377c4bcc055b5f7648e" +python-versions = "^3.11" +content-hash = "5ec23a1cb9e72e5d9ae6092f9fb2345fe38decb845c38979bd15612fe90883cb" diff --git a/pyproject.toml b/pyproject.toml index e4723ba673..c2b5ad8f6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,33 +8,33 @@ authors = [ readme = "README.md" [tool.poetry.dependencies] -python = "^3.10" -Flask = "<2.2.0" -lxml = "<4.7.1" +python = "^3.11" +flask = "^2.3.2" +lxml = "^4.9.2" webargs = "5.5" -wheel = "^0.37.1" -setuptools = "^62.3.2" -attrs = "^21.4.0" -Authlib = "1.0.0rc1" -autopep8 = "^1.6.0" +wheel = "^0.40.0" +setuptools = "^68.0.0" +attrs = "^23.1.0" +authlib = "^1.2.1" +autopep8 = "^2.0.2" beautifulsoup4 = "^4.11.1" -bcrypt = "^3.2.2" -bleach = "^5.0.0" -celery = { version = "4.4", extras = ["redis"] } +bcrypt = "^4.0.1" +bleach = "^6.0.0" +celery = {extras = ["redis"], version = "^5.3.1"} cffi = "^1.15.0" cssselect = "^1.1.0" docformatter = "^1.4" filelock = "^3.7.1" Flask-Assets = "^2.0" -Flask-Caching = "^2.0.1" +Flask-Caching = "^2.0.2" Flask-Compress = "^1.12" -Flask-Migrate = "^3.1.0" +Flask-Migrate = "^4.0.4" flask-oidc = "^1.4.0" Flask-OpenID = "^1.3.0" -Flask-SQLAlchemy = "^2.5.1" +Flask-SQLAlchemy = "^3.0.5" Flask-Testing = "^0.8.1" Flask-WTF = "^1.0.1" -gevent = "^21.12.0" +gevent = "^22.10.2" webassets = { git = "https://github.com/miracle2k/webassets.git" } gunicorn = "^20.1.0" html5lib = "^1.1" @@ -42,55 +42,55 @@ httpagentparser = "^1.9.2" humanize = "^4.1.0" isodate = "^0.6.1" langcodes = { extras = ["data"], version = "^3.3.0" } -libsass = "^0.21.0" +libsass = "^0.22.0" mailmanclient = "^3.3.3" marshmallow = "^3.16.0" marshmallow-enum = "^1.5.1" marshmallow-union = "^0.1.15.post1" -mmh3 = "^3.0.0" +mmh3 = "^4.0.0" pandocfilters = "^1.5.0" -Pillow = "^9.1.1" +pillow = "^10.0.0" psycogreen = "^1.0.2" psycopg2-binary = "^2.9.3" -pyaml = "^21.10.1" +pyaml = "^23.5.9" PyLaTeX = "^1.4.1" pypandoc = "^1.8.1" python-dateutil = "^2.8.2" python-magic = "^0.4.26" -pytz = "^2022.1" +pytz = "^2023.3" recommonmark = "^0.7.1" -responses = "^0.21.0" +responses = "^0.23.1" selenium = "^4.2.0" -Sphinx = "^5.0.1" -SQLAlchemy = "<1.4.0" -SQLAlchemy-Utils = "^0.38.2" -typing-inspect = "^0.7.1" +Sphinx = "^7.0.1" +SQLAlchemy = "<2.0.0" +SQLAlchemy-Utils = "^0.41.1" +typing-inspect = "^0.9.0" voikko = "^0.5" Wand = "^0.6.7" requests = "^2.27.1" six = "^1.16.0" python-gnupg = "^0.5.0" pysaml2 = "^7.2.1" +cachelib = "<0.10.0" qulacs = "^0.6.1" -werkzeug = "2.2.2" [tool.poetry.group.dev.dependencies] -mypy = "^0.981" -mypy-extensions = "^0.4.3" -types-bleach = "^5.0.2" +mypy = "^1.4.1" +mypy-extensions = "^1.0.0" +types-bleach = "^6.0.0.3" types-filelock = "^3.2.6" types-python-dateutil = "^2.8.17" -types-pytz = "^2021.3.8" +types-pytz = "^2023.3.0.0" types-PyYAML = "^6.0.8" types-redis = "^4.2.6" types-requests = "^2.27.30" types-attrs = "^19.1.0" marshmallow = "^3.16.0" -bcrypt = "^3.2.2" -Flask = "<2.2.0" -Authlib = "1.0.0rc1" +bcrypt = "^4.0.1" +flask = "^2.3.2" +authlib = "^1.2.1" langcodes = "^3.3.0" -black = "^22.6.0" +black = {extras = ["d"], version = "^23.3.0"} types-pysaml2 = "^1.0.0" qulacs = "^0.6.1" diff --git a/timApp/Dockerfile b/timApp/Dockerfile index 87e87a582c..52f059c51e 100755 --- a/timApp/Dockerfile +++ b/timApp/Dockerfile @@ -1,7 +1,9 @@ FROM ubuntu:22.04 as base +LABEL org.opencontainers.image.authors="tim@jyu.fi" +LABEL org.opencontainers.image.source="https://github.com/TIM-JYU/TIM" ENV APT_INSTALL="DEBIAN_FRONTEND=noninteractive apt-get -qq update && DEBIAN_FRONTEND=noninteractive apt-get -q install --no-install-recommends -y" \ - APT_CLEANUP="rm -rf /var/lib/apt/lists /dvisvgm-2.4 /usr/share/doc ~/.cache" + APT_CLEANUP="rm -rf /var/lib/apt/lists /usr/share/doc ~/.cache" # Configure timezone and locale RUN bash -c "${APT_INSTALL} locales tzdata && ${APT_CLEANUP}" @@ -95,10 +97,12 @@ FROM base as complete # Install Python, pip and other necessary packages +ENV PY_VERSION=3.11 +ENV PYTHON=python$PY_VERSION RUN bash -c "add-apt-repository -y ppa:deadsnakes/ppa && ${APT_CLEANUP}" -RUN bash -c "${APT_INSTALL} python3.10 python3.10-distutils && ${APT_CLEANUP}" +RUN bash -c "${APT_INSTALL} ${PYTHON} ${PYTHON}-distutils && ${APT_CLEANUP}" -RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 20 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/${PYTHON} 20 # lxml dependencies # C-parser for PyYAML @@ -116,18 +120,18 @@ RUN bash -c "${APT_INSTALL} \ libxmlsec1-openssl \ libxslt-dev \ libyaml-dev \ - python3.10-dev \ +${PYTHON}-dev \ voikko-fi \ zlib1g-dev \ && ${APT_CLEANUP}" -RUN wget -q https://bootstrap.pypa.io/get-pip.py && python3.10 get-pip.py && rm get-pip.py +RUN wget -q https://bootstrap.pypa.io/get-pip.py && ${PYTHON} get-pip.py && rm get-pip.py # Install xmlsec binary which is needed for PySAML2 RUN bash -c "${APT_INSTALL} xmlsec1 && ${APT_CLEANUP}" -ENV PIP_INSTALL="python3.10 -m pip install" -RUN bash -c "${PIP_INSTALL} --upgrade poetry==1.3.1 requests && ${APT_CLEANUP}" +ENV PIP_INSTALL="${PYTHON} -m pip install" +RUN bash -c "${PIP_INSTALL} --upgrade --ignore-installed poetry==1.5.1 requests blinker && ${APT_CLEANUP}" COPY pyproject.toml poetry.lock ./ RUN bash -c " \ @@ -166,9 +170,9 @@ RUN (curl -sL https://deb.nodesource.com/setup_16.x | bash -) && bash -c "${APT_ RUN npm i npm@6 -g && bash -c "${APT_CLEANUP}" # Flask-Testing does not let us configure host, so we do it here. -RUN sed -i "s/port=port, use_reloader=False/host='0.0.0.0', port=port, use_reloader=False/" /usr/local/lib/python3.10/dist-packages/flask_testing/utils.py +RUN sed -i "s/port=port, use_reloader=False/host='0.0.0.0', port=port, use_reloader=False/" /usr/local/lib/${PYTHON}/dist-packages/flask_testing/utils.py # Fix newest Werkzeug incompatibility -RUN sed -i "s/from werkzeug import cached_property/from werkzeug.utils import cached_property/" /usr/local/lib/python3.10/dist-packages/flask_testing/utils.py +RUN sed -i "s/from werkzeug import cached_property/from werkzeug.utils import cached_property/" /usr/local/lib/${PYTHON}/dist-packages/flask_testing/utils.py RUN wget -q https://www.texlive.info/CTAN/support/latexmk/latexmk.pl -O /usr/bin/latexmk diff --git a/timApp/modules/cs/Dockerfile b/timApp/modules/cs/Dockerfile index 3d3e5c05ac..ea5b1c0319 100755 --- a/timApp/modules/cs/Dockerfile +++ b/timApp/modules/cs/Dockerfile @@ -30,11 +30,17 @@ RUN echo "Europe/Helsinki" > /etc/timezone; dpkg-reconfigure -f noninteractive t # Install gpg and gpg-agent for verifying signatures before installing python RUN bash -c "${APT_INSTALL} gpg-agent && ${APT_CLEANUP}" -ENV PY_VERSION=3.10 +ENV PY_VERSION=3.11 ENV PYTHON=python$PY_VERSION + +# Install Python and pip +# Also fix potential ModuleNotFoundError: No module named 'apt_pkg' error caused by apt_pkg +# being targeted to some other Python version RUN bash -c "add-apt-repository ppa:deadsnakes/ppa && apt-get update && \ ${APT_INSTALL} ${PYTHON} wget dirmngr gpg-agent curl ${PYTHON}-distutils && \ - wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py && ${PYTHON} get-pip.py && rm get-pip.py && ${APT_CLEANUP}" + cd /usr/lib/python3/dist-packages/ && cp apt_pkg*.so apt_pkg.so && \ + wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py && ${PYTHON} get-pip.py && rm get-pip.py && \ + ${APT_CLEANUP}" RUN ${PYTHON} -m pip install --ignore-installed Flask bleach lxml && bash -c "${APT_CLEANUP}" RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && bash -c "${APT_CLEANUP}" From 7ad6d28538f09a02a1b48c37d369d635b398c264 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Tue, 4 Jul 2023 16:06:01 +0300 Subject: [PATCH 02/34] Refactor to support SQLAlchemy 1.4 * Mark relationship overlaps * Remove TimDb class as unused and unsupported --- timApp/answer/answer.py | 1 + .../celery_sqlalchemy_scheduler/schedulers.py | 15 +- timApp/document/editing/routes.py | 15 +- timApp/document/translation/translator.py | 2 + timApp/item/block.py | 2 + timApp/lecture/askedquestion.py | 6 +- timApp/plugin/calendar/models.py | 2 + timApp/tim_app.py | 8 +- timApp/timdb/dbaccess.py | 29 ++- timApp/timdb/init.py | 234 +++++++++--------- timApp/timdb/timdb.py | 218 ++++++++-------- timApp/user/user.py | 40 ++- timApp/user/usergroup.py | 12 +- timApp/velp/annotation_model.py | 2 +- timApp/velp/velp_models.py | 5 +- tim_common/timjsonencoder.py | 10 + tim_common/utils.py | 6 + 17 files changed, 329 insertions(+), 278 deletions(-) diff --git a/timApp/answer/answer.py b/timApp/answer/answer.py index 3c9905c6b6..1729b1e75a 100644 --- a/timApp/answer/answer.py +++ b/timApp/answer/answer.py @@ -65,6 +65,7 @@ class Answer(db.Model): back_populates="answers_alt", order_by="User.real_name", lazy="select", + overlaps="users", ) annotations = db.relationship("Annotation", back_populates="answer") saver = db.relationship( diff --git a/timApp/celery_sqlalchemy_scheduler/schedulers.py b/timApp/celery_sqlalchemy_scheduler/schedulers.py index 69b952b641..7769fe0eea 100644 --- a/timApp/celery_sqlalchemy_scheduler/schedulers.py +++ b/timApp/celery_sqlalchemy_scheduler/schedulers.py @@ -5,12 +5,10 @@ from celery import current_app from celery import schedules from celery.beat import Scheduler, ScheduleEntry -from celery.five import values, items -from celery.utils.encoding import safe_str, safe_repr from celery.utils.log import get_logger from celery.utils.time import maybe_make_aware +from kombu.utils.encoding import safe_str, safe_repr from kombu.utils.json import dumps, loads -from multiprocessing.util import Finalize from .models import ( PeriodicTask, @@ -22,6 +20,8 @@ from .session import SessionManager from .session import session_cleanup +# from multiprocessing.util import Finalize + # This scheduler must wake up more frequently than the # regular of 5 minutes because it needs to take external # changes to the schedule into account. @@ -310,7 +310,6 @@ def __repr__(self): class DatabaseScheduler(Scheduler): - Entry = ModelEntry Model = PeriodicTask Changes = PeriodicTaskChanged @@ -331,7 +330,7 @@ def __init__(self, *args, **kwargs): self._dirty = set() Scheduler.__init__(self, *args, **kwargs) - self._finalize = Finalize(self, self.sync, exitpriority=5) + # self._finalize = Finalize(self, self.sync, exitpriority=5) self.max_interval = ( kwargs.get("max_interval") or self.app.conf.beat_max_loop_interval @@ -402,7 +401,7 @@ def sync(self): self.schedule[name].save() # save to database logger.debug(f"{name} save to database") _tried.add(name) - except (KeyError) as exc: + except KeyError as exc: logger.error(exc) _failed.add(name) except sqlalchemy.exc.IntegrityError as exc: @@ -415,7 +414,7 @@ def sync(self): def update_from_dict(self, mapping): s = {} - for name, entry_fields in items(mapping): + for name, entry_fields in mapping.items(): # {'task': 'celery.backend_cleanup', # 'schedule': schedules.crontab('0', '4', '*'), # 'options': {'expires': 43200}} @@ -472,7 +471,7 @@ def schedule(self): if logger.isEnabledFor(logging.DEBUG): logger.debug( "Current schedule:\n%s", - "\n".join(repr(entry) for entry in values(self._schedule)), + "\n".join(repr(entry) for entry in self._schedule.values()), ) # logger.debug(self._schedule) return self._schedule diff --git a/timApp/document/editing/routes.py b/timApp/document/editing/routes.py index 92cff45ad0..a98518ecc9 100644 --- a/timApp/document/editing/routes.py +++ b/timApp/document/editing/routes.py @@ -55,7 +55,8 @@ from timApp.plugin.qst.qst import question_convert_js_to_yaml from timApp.plugin.save_plugin import save_plugin from timApp.readmark.readings import mark_read -from timApp.timdb.dbaccess import get_timdb + +# from timApp.timdb.dbaccess import get_timdb from timApp.timdb.exceptions import TimDbException from timApp.timdb.sqa import db from timApp.upload.uploadedfile import UploadedFile @@ -80,7 +81,7 @@ def update_document(doc_id): :return: A JSON object containing the versions of the document. """ - timdb = get_timdb() + # timdb = get_timdb() docentry = get_doc_or_abort(doc_id) verify_edit_access(docentry) if "file" in request.files: @@ -149,12 +150,10 @@ def update_document(doc_id): except (TimDbException, ValidationException) as e: raise RouteException(str(e)) pars = doc.get_paragraphs() - return manage_response(docentry, pars, timdb, ver_before) + return manage_response(docentry, pars, ver_before) -def manage_response( - docentry: DocInfo, pars: list[DocParagraph], timdb, ver_before: Version -): +def manage_response(docentry: DocInfo, pars: list[DocParagraph], ver_before: Version): doc = docentry.document_as_current_user chg = doc.get_changelog() notify_doc_watchers( @@ -169,7 +168,7 @@ def manage_response( @edit_page.post("/postNewTaskNames/") def rename_task_ids(): - timdb = get_timdb() + # timdb = get_timdb() doc_id, duplicates = verify_json_params("docId", "duplicates") manage_view = verify_json_params("manageView", require=False, default=False) docinfo = get_doc_or_abort(doc_id) @@ -230,7 +229,7 @@ def rename_task_ids(): update_cache=current_app.config["IMMEDIATE_PRELOAD"], ) else: - return manage_response(docinfo, pars, timdb, ver_before) + return manage_response(docinfo, pars, ver_before) @edit_page.post("/postParagraphQ/") diff --git a/timApp/document/translation/translator.py b/timApp/document/translation/translator.py index 48f4f5bcbd..a7f6c46f1e 100644 --- a/timApp/document/translation/translator.py +++ b/timApp/document/translation/translator.py @@ -230,6 +230,8 @@ def to_json(self) -> dict: class RegisteredTranslationService(TranslationService): """A translation service whose use is constrained by user group.""" + __abstract__ = True + def register(self, user_group: UserGroup) -> None: """ Set some state to the service object based on user group. diff --git a/timApp/item/block.py b/timApp/item/block.py index 140d1d432d..fcb85199dd 100644 --- a/timApp/item/block.py +++ b/timApp/item/block.py @@ -80,6 +80,7 @@ class Block(db.Model): primaryjoin=id == BlockAssociation.__table__.c.child, secondaryjoin=id == BlockAssociation.__table__.c.parent, lazy="select", + overlaps="children", ) notifications = db.relationship( "Notification", back_populates="block", lazy="dynamic" @@ -95,6 +96,7 @@ class Block(db.Model): secondary=UserGroupDoc.__table__, lazy="select", uselist=False, + overlaps="admin_doc", ) # If this Block corresponds to a message list's manage document, indicates the message list diff --git a/timApp/lecture/askedquestion.py b/timApp/lecture/askedquestion.py index c468fdedef..af56c2ad30 100644 --- a/timApp/lecture/askedquestion.py +++ b/timApp/lecture/askedquestion.py @@ -1,10 +1,8 @@ import json from contextlib import contextmanager from datetime import timedelta, datetime -from typing import Optional from sqlalchemy import func -from sqlalchemy.exc import InvalidRequestError from timApp.lecture.askedjson import AskedJson from timApp.lecture.lecture import Lecture @@ -39,7 +37,9 @@ class AskedQuestion(db.Model): answers = db.relationship( "LectureAnswer", back_populates="asked_question", lazy="dynamic" ) - answers_all = db.relationship("LectureAnswer", back_populates="asked_question") + answers_all = db.relationship( + "LectureAnswer", back_populates="asked_question", overlaps="answers" + ) running_question = db.relationship( "Runningquestion", back_populates="asked_question", lazy="select", uselist=False ) diff --git a/timApp/plugin/calendar/models.py b/timApp/plugin/calendar/models.py index d6876dc514..d86c1adba5 100644 --- a/timApp/plugin/calendar/models.py +++ b/timApp/plugin/calendar/models.py @@ -201,6 +201,7 @@ class Event(db.Model): Enrollment.__table__, primaryjoin=event_id == Enrollment.event_id, lazy="select", + overlaps="event, usergroup", ) """List of usergroups that are enrolled in the event""" @@ -209,6 +210,7 @@ class Event(db.Model): lazy="select", back_populates="event", cascade="all, delete-orphan", + overlaps="enrolled_users", ) """Enrollment information for the event""" diff --git a/timApp/tim_app.py b/timApp/tim_app.py index c38484521c..caf7f9fea6 100644 --- a/timApp/tim_app.py +++ b/timApp/tim_app.py @@ -134,7 +134,7 @@ LabelInVelp, AnnotationComment, ) -from tim_common.timjsonencoder import TimJsonEncoder +from tim_common.timjsonencoder import TimJsonProvider # All SQLAlchemy models must be imported in this module. all_models = ( @@ -238,6 +238,9 @@ sys.setrecursionlimit(10000) app = Flask(__name__) +app.json = TimJsonProvider(app) +app.json_provider_class = TimJsonProvider + # The autoescape setting needs to be forced because the template file extension used in TIM is jinja2. # The more accurate file extension helps IDEs recognize the file type better. app.jinja_env.autoescape = True @@ -272,9 +275,6 @@ app.jinja_env.add_extension("jinja2.ext.do") mimetypes.add_type("text/plain", ".scss") - -app.json_encoder = TimJsonEncoder - # Caddy sets the following headers: # X-Forwarded-For: # X-Forwarded-Proto: diff --git a/timApp/timdb/dbaccess.py b/timApp/timdb/dbaccess.py index 94292ca703..9d8bb562ff 100644 --- a/timApp/timdb/dbaccess.py +++ b/timApp/timdb/dbaccess.py @@ -1,22 +1,21 @@ from functools import cache from pathlib import Path -from flask import g, request -from timApp.timdb.timdb import TimDb - - -def get_timdb() -> TimDb: - """Returns the TimDb object and stores it in the Flask g object.""" - if not hasattr(g, "timdb"): - from timApp.auth.sessioninfo import get_current_user_object - - g.timdb = TimDb( - files_root_path=get_files_path(), - current_user_name=get_current_user_object().name, - route_path=request.path, - ) - return g.timdb +# from timApp.timdb.timdb import TimDb +# +# +# def get_timdb() -> TimDb: +# """Returns the TimDb object and stores it in the Flask g object.""" +# if not hasattr(g, "timdb"): +# from timApp.auth.sessioninfo import get_current_user_object +# +# g.timdb = TimDb( +# files_root_path=get_files_path(), +# current_user_name=get_current_user_object().name, +# route_path=request.path, +# ) +# return g.timdb @cache diff --git a/timApp/timdb/init.py b/timApp/timdb/init.py index 7ff38d628b..49d397750a 100644 --- a/timApp/timdb/init.py +++ b/timApp/timdb/init.py @@ -9,6 +9,7 @@ from alembic.runtime.environment import EnvironmentContext from alembic.runtime.migration import MigrationContext from alembic.script import ScriptDirectory +from sqlalchemy.orm import Session from sqlalchemy_utils import database_exists, create_database from timApp.admin.language_cli import add_all_supported_languages @@ -31,7 +32,8 @@ from timApp.tim_app import app from timApp.timdb.dbaccess import get_files_path from timApp.timdb.sqa import db, get_tim_main_engine -from timApp.timdb.timdb import TimDb + +# from timApp.timdb.timdb import TimDb from timApp.user.settings.style_utils import ( OFFICIAL_STYLES_PATH, USER_STYLES_PATH, @@ -75,128 +77,136 @@ def initialize_database(create_docs: bool = True) -> None: was_created = postgre_create_database(db_uri) if was_created: log_info(f"Database {db_uri} was created.") - timdb = TimDb(files_root_path=files_root_path) - sess = timdb.session - if database_has_tables(): - pass - else: - log_info("Creating database tables...") - db.create_all() - if not app.config["TESTING"]: - with app.app_context(): - flask_migrate.stamp() - # Alembic disables loggers for some reason - enable_loggers() - sess.add(AccessTypeModel(id=1, name="view")) - sess.add(AccessTypeModel(id=2, name="edit")) - sess.add(AccessTypeModel(id=3, name="teacher")) - sess.add(AccessTypeModel(id=4, name="manage")) - sess.add(AccessTypeModel(id=5, name="see answers")) - sess.add(AccessTypeModel(id=6, name="owner")) - sess.add(AccessTypeModel(id=7, name="copy")) - - create_enrollment_types(sess) - - create_special_usergroups(sess) - sess.add(UserGroup.create(app.config["HOME_ORGANIZATION"] + ORG_GROUP_SUFFIX)) - precomputed_hashes = [ - "$2b$04$zXpqPI7SNOWkbmYKb6QK9ePEUe.0pxZRctLybWNE1nxw0/WMiYlPu", # test1pass - "$2b$04$B0mE/VeD5Uzucfa2juzY5.8aObzCqQSDVK//bxdiQ5Ayv59PwWsVq", # test2pass - "$2b$04$ajl88D949ur6IF0OE7ZU2OLojkZiOwU5JtUkGTcBnwUi6W7ZIfXPe", # test3pass - ] - for i in range(1, 4): - u, _ = User.create_with_group( - UserInfo( - username=f"testuser{i}", - full_name=f"Test user {i}", - email=f"test{i}@example.com", - ) - ) - u.pass_ = precomputed_hashes[i - 1] - admin_group = UserGroup.get_admin_group() - - # Create users folder explicitly with admin as owner. - # Otherwise its owner would be whoever logs in to TIM instance first. - Folder.create("users", owner_groups=admin_group) - - if create_docs: - t1g = UserGroup.get_by_name("testuser1") - import_document_from_file( - static_tim_doc("initial/programming_examples.md"), - "tim/Eri-ohjelmointikielia", - t1g, - title="Eri ohjelmointikieliä", - ) - print_base = import_document_from_file( - static_tim_doc("initial/print_base.md"), - f"{TEMPLATE_FOLDER_NAME}/{PRINT_FOLDER_NAME}/base", - admin_group, - title="Default print template", - ) - print_base.block.add_rights( - [UserGroup.get_logged_in_group()], AccessType.view - ) - group_preamble = import_document_from_file( - static_tim_doc("initial/group_preamble.md"), - f"groups/{TEMPLATE_FOLDER_NAME}/{PREAMBLE_FOLDER_NAME}/{DEFAULT_PREAMBLE_DOC}", - admin_group, - title="preamble", - ) - group_preamble.block.add_rights( - [UserGroup.get_logged_in_group()], AccessType.view - ) + # timdb = TimDb(files_root_path=files_root_path) + # sess = timdb.session - messagelist_preamble = import_document_from_file( - static_tim_doc("initial/messagelist_preamble.md"), - f"{MESSAGE_LIST_DOC_PREFIX}/{TEMPLATE_FOLDER_NAME}/{PREAMBLE_FOLDER_NAME}/{DEFAULT_PREAMBLE_DOC}", - admin_group, - title="preamble", - ) - messagelist_preamble.block.add_rights( - [UserGroup.get_logged_in_group()], AccessType.view + with app.app_context(): + sess = db.session + if database_has_tables(): + pass + else: + log_info("Creating database tables...") + db.create_all() + if not app.config["TESTING"]: + with app.app_context(): + flask_migrate.stamp() + # Alembic disables loggers for some reason + enable_loggers() + sess.add(AccessTypeModel(id=1, name="view")) + sess.add(AccessTypeModel(id=2, name="edit")) + sess.add(AccessTypeModel(id=3, name="teacher")) + sess.add(AccessTypeModel(id=4, name="manage")) + sess.add(AccessTypeModel(id=5, name="see answers")) + sess.add(AccessTypeModel(id=6, name="owner")) + sess.add(AccessTypeModel(id=7, name="copy")) + + create_enrollment_types(sess) + + create_special_usergroups(sess) + sess.add( + UserGroup.create(app.config["HOME_ORGANIZATION"] + ORG_GROUP_SUFFIX) ) + precomputed_hashes = [ + "$2b$04$zXpqPI7SNOWkbmYKb6QK9ePEUe.0pxZRctLybWNE1nxw0/WMiYlPu", # test1pass + "$2b$04$B0mE/VeD5Uzucfa2juzY5.8aObzCqQSDVK//bxdiQ5Ayv59PwWsVq", # test2pass + "$2b$04$ajl88D949ur6IF0OE7ZU2OLojkZiOwU5JtUkGTcBnwUi6W7ZIfXPe", # test3pass + ] + for i in range(1, 4): + u, _ = User.create_with_group( + UserInfo( + username=f"testuser{i}", + full_name=f"Test user {i}", + email=f"test{i}@example.com", + ) + ) + u.pass_ = precomputed_hashes[i - 1] + admin_group = UserGroup.get_admin_group() + + # Create users folder explicitly with admin as owner. + # Otherwise its owner would be whoever logs in to TIM instance first. + Folder.create("users", owner_groups=admin_group) + + if create_docs: + t1g = UserGroup.get_by_name("testuser1") + import_document_from_file( + static_tim_doc("initial/programming_examples.md"), + "tim/Eri-ohjelmointikielia", + t1g, + title="Eri ohjelmointikieliä", + ) + print_base = import_document_from_file( + static_tim_doc("initial/print_base.md"), + f"{TEMPLATE_FOLDER_NAME}/{PRINT_FOLDER_NAME}/base", + admin_group, + title="Default print template", + ) + print_base.block.add_rights( + [UserGroup.get_logged_in_group()], AccessType.view + ) + group_preamble = import_document_from_file( + static_tim_doc("initial/group_preamble.md"), + f"groups/{TEMPLATE_FOLDER_NAME}/{PREAMBLE_FOLDER_NAME}/{DEFAULT_PREAMBLE_DOC}", + admin_group, + title="preamble", + ) + group_preamble.block.add_rights( + [UserGroup.get_logged_in_group()], AccessType.view + ) - admin_group = UserGroup.get_by_name(ADMIN_GROUPNAME) - error_codes_folder = Folder.create( - ERROR_CODES_FOLDER, admin_group, title="Error code database" - ) - grant_default_access( - [admin_group], error_codes_folder, AccessType.owner, BlockType.Document - ) + messagelist_preamble = import_document_from_file( + static_tim_doc("initial/messagelist_preamble.md"), + f"{MESSAGE_LIST_DOC_PREFIX}/{TEMPLATE_FOLDER_NAME}/{PREAMBLE_FOLDER_NAME}/{DEFAULT_PREAMBLE_DOC}", + admin_group, + title="preamble", + ) + messagelist_preamble.block.add_rights( + [UserGroup.get_logged_in_group()], AccessType.view + ) - verify_contact_message_template = import_document_from_file( - static_tim_doc("initial/contact_verify_message.md"), - "settings/verify-templates/contact", - admin_group, - title="Contact verify", - ) - verify_contact_message_template = import_document_from_file( - static_tim_doc("initial/primary_contact_verify_message.md"), - "settings/verify-templates/primary-contact", - admin_group, - title="New primary contact verify", - ) - verify_contact_message_template.document.set_settings( - {"subject": "Verify new contact", "textplain": True} - ) - verify_contact_message_template.block.add_rights( - [UserGroup.get_logged_in_group()], AccessType.view - ) + admin_group = UserGroup.get_by_name(ADMIN_GROUPNAME) + error_codes_folder = Folder.create( + ERROR_CODES_FOLDER, admin_group, title="Error code database" + ) + grant_default_access( + [admin_group], + error_codes_folder, + AccessType.owner, + BlockType.Document, + ) + + verify_contact_message_template = import_document_from_file( + static_tim_doc("initial/contact_verify_message.md"), + "settings/verify-templates/contact", + admin_group, + title="Contact verify", + ) + verify_contact_message_template = import_document_from_file( + static_tim_doc("initial/primary_contact_verify_message.md"), + "settings/verify-templates/primary-contact", + admin_group, + title="New primary contact verify", + ) + verify_contact_message_template.document.set_settings( + {"subject": "Verify new contact", "textplain": True} + ) + verify_contact_message_template.block.add_rights( + [UserGroup.get_logged_in_group()], AccessType.view + ) - create_style_docs() + create_style_docs() - # Add the machine-translators to database with their default values. - add_all_tr_services_to_session() + # Add the machine-translators to database with their default values. + add_all_tr_services_to_session() - # Create and add all supported languages to the database - add_all_supported_languages() + # Create and add all supported languages to the database + add_all_supported_languages() - sess.commit() - log_info("Database initialization done.") + sess.commit() + log_info("Database initialization done.") if not app.config["TESTING"]: exit_if_not_db_up_to_date() - timdb.close() + # timdb.close() def create_style_docs() -> tuple[list[Folder], list[DocInfo]]: diff --git a/timApp/timdb/timdb.py b/timApp/timdb/timdb.py index 7e64927718..2231a42c74 100644 --- a/timApp/timdb/timdb.py +++ b/timApp/timdb/timdb.py @@ -1,10 +1,4 @@ """Defines the TimDb database class.""" -import time -from pathlib import Path -from time import sleep - -from timApp.timdb.sqa import db -from timApp.util.logger import log_info, log_debug, log_error, log_warning num = 0 @@ -32,109 +26,109 @@ } -class TimDb: - """DEPRECATED CLASS, DO NOT ADD NEW CODE! - - Handles saving and retrieving information from TIM database. - """ - - instances = 0 - - def __init__( - self, - files_root_path: Path, - current_user_name: str = "Anonymous", - route_path: str = "", - ): - """Initializes TimDB with the specified files root path, SQLAlchemy session and user name. - - - :param current_user_name: The username of the current user. - :param files_root_path: The root path where all the files will be stored. - :param route_path: Path for the route requesting the db - - """ - self.files_root_path = files_root_path - self.route_path = route_path - self.current_user_name = current_user_name - - self.blocks_path = self.files_root_path / "blocks" - for path in [self.blocks_path]: - if not path.exists(): - log_info(f"Creating directory: {path}") - path.mkdir(parents=True, exist_ok=False) - self.reset_attrs() - - def reset_attrs(self): - self.num = 0 - self.time = 0 - self.engine = None - self.db = None - self.velps = None - self.velp_groups = None - - def __getattribute__(self, item): - """Used to open TimDb connection lazily.""" - if item in DB_PART_NAMES and self.db is None: - self.open() - return object.__getattribute__(self, item) - - def open(self): - global num - num += 1 - self.num = num - self.time = time.time() - log_debug( - f"GetDb {worker_pid:2d} {self.num:6d} {'':2s} {'':3s} {'':7s} {self.route_path:s}" - ) - # log_info('TimDb-dstr {:2d} {:6d} {:2d} {:3d} {:7.5f} {:s}'.format(worker_pid,self.num, TimDb.instances, bes, time.time() - self.time, self.route_path)) - waiting = False - from timApp.tim_app import app - - while True: - try: - self.engine = db.get_engine(app) - self.db = self.engine.connect().connection - self.session = db.session - break - except Exception as err: - if not waiting: - log_warning("WaitDb " + str(self.num) + " " + str(err)) - waiting = True - sleep(0.1) - - if waiting: - log_warning("ReadyDb " + str(self.num)) - - TimDb.instances += 1 - # num_connections = self.get_pg_connections() - # log_info('TimDb instances/PG connections: {}/{} (constructor)'.format(TimDb.instances, num_connections)) - - def get_pg_connections(self): - """Returns the number of clients currently connected to PostgreSQL.""" - cursor = self.db.cursor() - cursor.execute("SELECT sum(numbackends) FROM pg_stat_database") - num_connections = cursor.fetchone()[0] - return num_connections - - def commit(self): - """Commits any changes to the database.""" - db.session.commit() - if self.db: - self.db.commit() - - def close(self) -> None: - """Closes the database connection.""" - if hasattr(self, "db") and self.db is not None: - bes = -1 - TimDb.instances -= 1 - try: - # bes = self.get_pg_connections() - self.db.close() - except Exception as err: - log_error("close error: " + str(self.num) + " " + str(err)) - - log_debug( - f"TimDb-dstr {worker_pid:2d} {self.num:6d} {TimDb.instances:2d} {bes:3d} {time.time() - self.time:7.5f} {self.route_path:s}" - ) - self.reset_attrs() +# class TimDb: +# """DEPRECATED CLASS, DO NOT ADD NEW CODE! +# +# Handles saving and retrieving information from TIM database. +# """ +# +# instances = 0 +# +# def __init__( +# self, +# files_root_path: Path, +# current_user_name: str = "Anonymous", +# route_path: str = "", +# ): +# """Initializes TimDB with the specified files root path, SQLAlchemy session and user name. +# +# +# :param current_user_name: The username of the current user. +# :param files_root_path: The root path where all the files will be stored. +# :param route_path: Path for the route requesting the db +# +# """ +# self.files_root_path = files_root_path +# self.route_path = route_path +# self.current_user_name = current_user_name +# +# self.blocks_path = self.files_root_path / "blocks" +# for path in [self.blocks_path]: +# if not path.exists(): +# log_info(f"Creating directory: {path}") +# path.mkdir(parents=True, exist_ok=False) +# self.reset_attrs() +# +# def reset_attrs(self): +# self.num = 0 +# self.time = 0 +# self.engine = None +# self.db = None +# self.velps = None +# self.velp_groups = None +# +# def __getattribute__(self, item): +# """Used to open TimDb connection lazily.""" +# if item in DB_PART_NAMES and self.db is None: +# self.open() +# return object.__getattribute__(self, item) +# +# def open(self): +# global num +# num += 1 +# self.num = num +# self.time = time.time() +# log_debug( +# f"GetDb {worker_pid:2d} {self.num:6d} {'':2s} {'':3s} {'':7s} {self.route_path:s}" +# ) +# # log_info('TimDb-dstr {:2d} {:6d} {:2d} {:3d} {:7.5f} {:s}'.format(worker_pid,self.num, TimDb.instances, bes, time.time() - self.time, self.route_path)) +# waiting = False +# from timApp.tim_app import app +# +# while True: +# try: +# self.engine = db.get_engine(app) +# self.db = self.engine.connect().connection +# self.session = db.session +# break +# except Exception as err: +# if not waiting: +# log_warning("WaitDb " + str(self.num) + " " + str(err)) +# waiting = True +# sleep(0.1) +# +# if waiting: +# log_warning("ReadyDb " + str(self.num)) +# +# TimDb.instances += 1 +# # num_connections = self.get_pg_connections() +# # log_info('TimDb instances/PG connections: {}/{} (constructor)'.format(TimDb.instances, num_connections)) +# +# def get_pg_connections(self): +# """Returns the number of clients currently connected to PostgreSQL.""" +# cursor = self.db.cursor() +# cursor.execute("SELECT sum(numbackends) FROM pg_stat_database") +# num_connections = cursor.fetchone()[0] +# return num_connections +# +# def commit(self): +# """Commits any changes to the database.""" +# db.session.commit() +# if self.db: +# self.db.commit() +# +# def close(self) -> None: +# """Closes the database connection.""" +# if hasattr(self, "db") and self.db is not None: +# bes = -1 +# TimDb.instances -= 1 +# try: +# # bes = self.get_pg_connections() +# self.db.close() +# except Exception as err: +# log_error("close error: " + str(self.num) + " " + str(err)) +# +# log_debug( +# f"TimDb-dstr {worker_pid:2d} {self.num:6d} {TimDb.instances:2d} {bes:3d} {time.time() - self.time:7.5f} {self.route_path:s}" +# ) +# self.reset_attrs() diff --git a/timApp/user/user.py b/timApp/user/user.py index c8cc5db161..911967e139 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -296,6 +296,7 @@ class User(db.Model, TimeStampMixin, SCIMEntity): & (UserContact.channel == Channel.EMAIL), lazy="select", uselist=False, + overlaps="user, contacts", ) """ The primary email contact for the user. @@ -322,7 +323,10 @@ def _set_email(self, value: str) -> None: """User's consent changes.""" contacts: list[UserContact] = db.relationship( - "UserContact", back_populates="user", lazy="select" + "UserContact", + back_populates="user", + lazy="select", + overlaps="primary_email_contact", ) """User's contacts.""" @@ -337,6 +341,7 @@ def _set_email(self, value: str) -> None: primaryjoin=(id == UserGroupMember.user_id) & membership_current, back_populates="users", lazy="select", + overlaps="user, current_memberships, group, memberships, memberships_sel", ) """Current groups of the user is a member of.""" @@ -345,6 +350,7 @@ def _set_email(self, value: str) -> None: UserGroupMember.__table__, primaryjoin=id == UserGroupMember.user_id, lazy="dynamic", + overlaps="group, groups, user, users, current_memberships, memberships, memberships_sel", ) """All groups of the user as a dynamic query.""" @@ -353,6 +359,7 @@ def _set_email(self, value: str) -> None: UserGroupMember.__table__, primaryjoin=(id == UserGroupMember.user_id) & membership_deleted, lazy="dynamic", + overlaps="group, groups, groups_dyn, user, users, current_memberships, memberships, memberships_sel", ) """All groups the user is no longer a member of as a dynamic query.""" @@ -360,12 +367,14 @@ def _set_email(self, value: str) -> None: UserGroupMember, foreign_keys="UserGroupMember.user_id", lazy="dynamic", + overlaps="groups, groups_dyn, groups_inactive, user, users", ) """User's group memberships as a dynamic query.""" memberships: list[UserGroupMember] = db.relationship( UserGroupMember, foreign_keys="UserGroupMember.user_id", + overlaps="groups_inactive, memberships_dyn, user, users", ) """All user's group memberships.""" @@ -373,7 +382,7 @@ def _set_email(self, value: str) -> None: UserGroupMember, primaryjoin=(id == UserGroupMember.user_id) & membership_current, collection_class=attribute_mapped_collection("usergroup_id"), - # back_populates="group", + overlaps="groups, groups_dyn, groups_inactive, memberships, memberships_dyn, user, users", ) """Active group memberships mapped by user group ID.""" @@ -405,7 +414,11 @@ def _set_email(self, value: str) -> None: """User's activity during lectures.""" answers = db.relationship( - "Answer", secondary=UserAnswer.__table__, back_populates="users", lazy="dynamic" + "Answer", + secondary=UserAnswer.__table__, + back_populates="users", + lazy="dynamic", + overlaps="users_all", ) """User's answers to tasks as a dynamic query.""" @@ -426,17 +439,24 @@ def _set_email(self, value: str) -> None: "UserSession", primaryjoin=(id == UserSession.user_id) & ~UserSession.expired, collection_class=attribute_mapped_collection("session_id"), + overlaps="sessions, user", ) """Active sessions mapped by the session ID.""" # Used for copying - notifications_alt = db.relationship("Notification") - owned_lectures_alt = db.relationship("Lecture") - lectureanswers_alt = db.relationship("LectureAnswer") - messages_alt = db.relationship("Message") - answers_alt = db.relationship("Answer", secondary=UserAnswer.__table__) - annotations_alt = db.relationship("Annotation") - velps_alt = db.relationship("Velp") + notifications_alt = db.relationship("Notification", overlaps="notifications, user") + owned_lectures_alt = db.relationship("Lecture", overlaps="owned_lectures, owner") + lectureanswers_alt = db.relationship( + "LectureAnswer", overlaps="lectureanswers, user" + ) + messages_alt = db.relationship("Message", overlaps="messages, user") + answers_alt = db.relationship( + "Answer", + secondary=UserAnswer.__table__, + overlaps="answers, users", + ) + annotations_alt = db.relationship("Annotation", overlaps="annotations, annotator") + velps_alt = db.relationship("Velp", overlaps="velps, creator") def update_email( self, diff --git a/timApp/user/usergroup.py b/timApp/user/usergroup.py index 71d7ebd61a..d344ab294d 100644 --- a/timApp/user/usergroup.py +++ b/timApp/user/usergroup.py @@ -83,22 +83,26 @@ def scim_display_name(self): primaryjoin=(id == UserGroupMember.usergroup_id) & membership_current, secondaryjoin="UserGroupMember.user_id == User.id", back_populates="groups", + overlaps="group, user", ) memberships = db.relationship( UserGroupMember, back_populates="group", lazy="dynamic", + overlaps="users", ) memberships_sel = db.relationship( UserGroupMember, back_populates="group", cascade="all, delete-orphan", + overlaps="memberships, users", ) current_memberships: dict[int, UserGroupMember] = db.relationship( UserGroupMember, primaryjoin=(id == UserGroupMember.usergroup_id) & membership_current, collection_class=attribute_mapped_collection("user_id"), back_populates="group", + overlaps="memberships, memberships_sel, users", ) accesses = db.relationship( "BlockAccess", @@ -109,13 +113,17 @@ def scim_display_name(self): "BlockAccess", collection_class=attribute_mapped_collection("group_collection_key"), cascade="all, delete-orphan", + overlaps="accesses, usergroup", ) readparagraphs = db.relationship( "ReadParagraph", back_populates="usergroup", lazy="dynamic" ) - readparagraphs_alt = db.relationship("ReadParagraph") + readparagraphs_alt = db.relationship( + "ReadParagraph", + overlaps="readparagraphs, usergroup", + ) notes = db.relationship("UserNote", back_populates="usergroup", lazy="dynamic") - notes_alt = db.relationship("UserNote") + notes_alt = db.relationship("UserNote", overlaps="notes, usergroup") admin_doc: Block = db.relationship( "Block", diff --git a/timApp/velp/annotation_model.py b/timApp/velp/annotation_model.py index b1d60e3d55..52bee96419 100644 --- a/timApp/velp/annotation_model.py +++ b/timApp/velp/annotation_model.py @@ -1,7 +1,6 @@ import json from dataclasses import dataclass from datetime import datetime -from typing import Optional from timApp.timdb.sqa import db @@ -142,6 +141,7 @@ class Annotation(db.Model): velp_content = db.relationship( "VelpContent", primaryjoin="VelpContent.version_id == foreign(Annotation.velp_version_id)", + overlaps="velp_version", ) def set_position_info(self, coordinates: AnnotationPosition) -> None: diff --git a/timApp/velp/velp_models.py b/timApp/velp/velp_models.py index a8d892387a..669833d040 100644 --- a/timApp/velp/velp_models.py +++ b/timApp/velp/velp_models.py @@ -1,6 +1,5 @@ """Defines all data models related to velps.""" from datetime import datetime -from typing import Dict, Any from sqlalchemy.orm.collections import attribute_mapped_collection # type: ignore @@ -256,5 +255,5 @@ class VelpVersion(db.Model): db.DateTime(timezone=True), nullable=False, default=datetime.utcnow ) - velp: Velp = db.relationship("Velp") - content: list[VelpContent] = db.relationship("VelpContent") + velp: Velp = db.relationship("Velp", overlaps="velp_versions") + content: list[VelpContent] = db.relationship("VelpContent", overlaps="velp_version") diff --git a/tim_common/timjsonencoder.py b/tim_common/timjsonencoder.py index 1c05ff1feb..2bf24d29ef 100644 --- a/tim_common/timjsonencoder.py +++ b/tim_common/timjsonencoder.py @@ -2,7 +2,9 @@ import json from dataclasses import is_dataclass, fields from enum import Enum +from typing import Any +from flask.json.provider import JSONProvider from isodate import duration_isoformat from isodate.duration import Duration from jinja2 import Undefined @@ -16,6 +18,14 @@ sqlalchemy_imported = False +class TimJsonProvider(JSONProvider): + def dumps(self, obj: Any, **kwargs: Any) -> str: + return json.dumps(obj, cls=TimJsonEncoder, **kwargs) + + def loads(self, s: str | bytes, **kwargs: Any) -> Any: + return json.loads(s, **kwargs) + + class TimJsonEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, datetime.datetime): diff --git a/tim_common/utils.py b/tim_common/utils.py index 278b68ddc8..9e6b6bc8cc 100644 --- a/tim_common/utils.py +++ b/tim_common/utils.py @@ -1,3 +1,4 @@ +from dataclasses import field from typing import Any, Mapping import marshmallow @@ -7,6 +8,11 @@ from marshmallow.utils import _Missing Missing = _Missing +Missing.__hash__ = lambda self: id(self) # type: ignore + +# + +missing_field = field(default_factory=lambda: marshmallow.missing) # type: ignore _BoolField = Boolean() From b4d779f23a1575f37d9d75fcf7b5174c6a43985d Mon Sep 17 00:00:00 2001 From: dezhidki Date: Tue, 4 Jul 2023 18:30:17 +0300 Subject: [PATCH 03/34] Refactor DB queries to use Select API The API is used by SQLAlchemy 2.0 as the primary approach to querying --- poetry.lock | 16 +- timApp/admin/answer_cli.py | 120 +++++++----- timApp/admin/associate_old_uploads.py | 15 +- timApp/admin/change_group_email.py | 7 +- timApp/admin/fix_imagex_freehanddata.py | 10 +- timApp/admin/fix_orphan_documents.py | 18 +- timApp/admin/item_cli.py | 38 ++-- timApp/admin/language_cli.py | 15 +- timApp/admin/routes.py | 9 +- timApp/admin/translationservice_cli.py | 6 +- timApp/admin/user_cli.py | 39 ++-- timApp/answer/answers.py | 172 ++++++++++-------- timApp/answer/backup.py | 14 +- timApp/answer/routes.py | 148 +++++++++------ timApp/auth/access/routes.py | 18 +- timApp/auth/accesshelper.py | 32 ++-- timApp/auth/login.py | 28 ++- timApp/auth/oauth2/oauth2.py | 38 ++-- timApp/auth/session/routes.py | 26 +-- timApp/auth/session/util.py | 70 ++++--- timApp/auth/sessioninfo.py | 20 +- timApp/backup/backup_routes.py | 7 +- timApp/bookmark/course.py | 12 +- timApp/celery_sqlalchemy_scheduler/models.py | 32 ++-- .../celery_sqlalchemy_scheduler/schedulers.py | 17 +- timApp/document/changelog.py | 12 +- timApp/document/changelogentry.py | 3 +- timApp/document/docentry.py | 43 +++-- timApp/document/docinfo.py | 21 ++- timApp/document/documents.py | 23 ++- timApp/document/editing/routes.py | 4 +- timApp/document/translation/deepl.py | 8 +- timApp/document/translation/language.py | 5 +- timApp/document/translation/routes.py | 68 ++++--- timApp/document/translation/translator.py | 18 +- timApp/folder/folder.py | 40 ++-- timApp/gamification/gamificationdata.py | 5 +- timApp/item/distribute_rights.py | 24 +-- timApp/item/item.py | 21 ++- timApp/item/manage.py | 81 ++++++--- timApp/item/routes.py | 26 ++- timApp/item/routes_tags.py | 36 ++-- timApp/item/taskblock.py | 16 +- timApp/lecture/askedjson.py | 8 +- timApp/lecture/askedquestion.py | 6 +- timApp/lecture/lecture.py | 28 ++- timApp/lecture/lectureanswer.py | 14 +- timApp/lecture/routes.py | 77 ++++---- timApp/messaging/messagelist/emaillist.py | 24 ++- .../messagelist/messagelist_models.py | 31 +++- .../messagelist/messagelist_utils.py | 23 ++- .../timMessage/internalmessage_models.py | 12 +- timApp/messaging/timMessage/routes.py | 83 ++++++--- .../422ab312e579_membership_end_added_by.py | 6 +- ...e136511e_make_special_folders_lowercase.py | 13 +- .../ef104a711321_add_scimusergroup_table.py | 14 +- timApp/note/notes.py | 16 +- timApp/note/routes.py | 30 +-- timApp/note/usernote.py | 4 +- timApp/notification/notify.py | 14 +- timApp/notification/pending_notification.py | 10 +- timApp/peerreview/util/groups.py | 16 +- timApp/peerreview/util/peerreview_utils.py | 84 ++++++--- timApp/plugin/calendar/calendar.py | 77 +++++--- timApp/plugin/calendar/models.py | 71 +++++--- timApp/plugin/group_join/group_join.py | 12 +- timApp/plugin/importdata/importData.py | 23 ++- timApp/plugin/jsrunner/util.py | 27 ++- timApp/plugin/plugin.py | 34 +++- timApp/plugin/pluginControl.py | 3 +- timApp/plugin/plugintype.py | 13 +- timApp/plugin/tableform/tableForm.py | 21 ++- timApp/plugin/timtable/timTable.py | 8 +- timApp/plugin/userselect/action_queue.py | 26 ++- timApp/plugin/userselect/userselect.py | 61 +++++-- timApp/printing/documentprinter.py | 26 +-- timApp/readmark/readings.py | 75 +++++--- timApp/readmark/routes.py | 45 +++-- timApp/scheduling/scheduling_routes.py | 60 ++++-- timApp/sisu/scim.py | 36 ++-- timApp/sisu/sisu.py | 8 +- timApp/slide/routes.py | 8 +- timApp/tests/browser/test_model_answer.py | 4 +- timApp/tests/browser/test_questions.py | 10 +- timApp/tests/browser/test_reviewcanvas.py | 4 +- timApp/tests/browser/test_velps.py | 2 +- timApp/tests/db/test_personal_folder.py | 14 +- timApp/tests/db/test_plugin.py | 5 +- timApp/tests/db/test_users.py | 4 +- timApp/tests/server/test_autocounters.py | 4 +- timApp/tests/server/test_comments.py | 13 +- timApp/tests/server/test_default_rights.py | 7 +- timApp/tests/server/test_duration.py | 50 +++-- timApp/tests/server/test_jsrunner.py | 6 +- timApp/tests/server/test_lecture.py | 7 +- timApp/tests/server/test_macros.py | 4 +- timApp/tests/server/test_notify.py | 4 +- timApp/tests/server/test_peer_review.py | 43 +++-- timApp/tests/server/test_plugins.py | 50 +++-- timApp/tests/server/test_plugins_preamble.py | 9 +- timApp/tests/server/test_readings.py | 22 ++- timApp/tests/server/test_self_expire.py | 8 +- timApp/tests/server/test_signup.py | 28 ++- timApp/tests/server/test_tim_message.py | 29 ++- timApp/tests/server/test_translation.py | 23 ++- timApp/tests/server/test_user_sessions.py | 64 +++++-- timApp/tests/server/test_velp.py | 78 ++++++-- timApp/tests/server/test_verification.py | 27 ++- timApp/tests/server/timroutetest.py | 15 +- timApp/tim_celery.py | 22 ++- timApp/upload/upload.py | 29 ++- timApp/upload/uploadedfile.py | 16 +- timApp/user/contacts.py | 49 +++-- timApp/user/groups.py | 23 ++- timApp/user/hakaorganization.py | 11 +- timApp/user/personaluniquecode.py | 16 +- timApp/user/preferences.py | 8 +- timApp/user/settings/settings.py | 59 ++++-- timApp/user/settings/styles.py | 9 +- timApp/user/user.py | 119 +++++++----- timApp/user/usergroup.py | 65 +++++-- timApp/user/users.py | 15 +- timApp/user/userutils.py | 22 ++- timApp/user/verification/routes.py | 15 +- timApp/user/verification/verification.py | 27 ++- timApp/util/flask/search.py | 23 ++- timApp/util/get_fields.py | 76 +++++--- timApp/velp/annotation.py | 11 +- timApp/velp/annotations.py | 60 +++--- timApp/velp/velp.py | 118 ++++++++---- timApp/velp/velpgroups.py | 169 ++++++++++++----- timApp/velp/velps.py | 62 ++++--- 132 files changed, 2677 insertions(+), 1409 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6140fb98f3..799f0b2888 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3122,6 +3122,20 @@ test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3 timezone = ["python-dateutil"] url = ["furl (>=0.4.1)"] +[[package]] +name = "sqlalchemy2-stubs" +version = "0.0.2a34" +description = "Typing Stubs for SQLAlchemy 1.4" +optional = false +python-versions = ">=3.6" +files = [ + {file = "sqlalchemy2-stubs-0.0.2a34.tar.gz", hash = "sha256:2432137ab2fde1a608df4544f6712427b0b7ff25990cfbbc5a9d1db6c8c6f489"}, + {file = "sqlalchemy2_stubs-0.0.2a34-py3-none-any.whl", hash = "sha256:a313220ac793404349899faf1272e821a62dbe1d3a029bd444faa8d3e966cd07"}, +] + +[package.dependencies] +typing-extensions = ">=3.7.4" + [[package]] name = "trio" version = "0.22.1" @@ -3690,4 +3704,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "5ec23a1cb9e72e5d9ae6092f9fb2345fe38decb845c38979bd15612fe90883cb" +content-hash = "48dc1f000dc504a52e0c0f5a2e26a372ed0ab67b4192f78c035f3f8c96d0a623" diff --git a/timApp/admin/answer_cli.py b/timApp/admin/answer_cli.py index f653947c57..ce5401fccf 100644 --- a/timApp/admin/answer_cli.py +++ b/timApp/admin/answer_cli.py @@ -2,11 +2,11 @@ import sys from dataclasses import dataclass from datetime import datetime -from typing import Sequence, Optional +from typing import Sequence import click from flask.cli import AppGroup -from sqlalchemy import func +from sqlalchemy import func, select, delete from sqlalchemy.orm import joinedload from timApp.admin.datetimetype import DateTimeType @@ -20,6 +20,7 @@ from timApp.item.block import Block from timApp.item.item import Item from timApp.plugin.taskid import TaskId +from timApp.timdb.sqa import db from timApp.upload.uploadedfile import PluginUpload from timApp.user.user import User from timApp.user.usergroup import UserGroup @@ -37,12 +38,16 @@ @answer_cli.command() @click.option("--dry-run/--no-dry-run", default=True) def fix_double_c(dry_run: bool) -> None: - answers: list[Answer] = ( - Answer.query.filter( - (Answer.answered_on > datetime(year=2020, month=2, day=9)) - & Answer.content.startswith('{"c": {"c":') + answers = ( + db.session.execute( + select(Answer) + .filter( + (Answer.answered_on > datetime(year=2020, month=2, day=9)) + & Answer.content.startswith('{"c": {"c":') + ) + .order_by(Answer.id) ) - .order_by(Answer.id) + .scalars() .all() ) count = 0 @@ -74,11 +79,10 @@ class AnswerDeleteResult: @click.argument("doc", type=TimDocumentType()) @click.option("--dry-run/--no-dry-run", default=True) def clear_all(doc: DocInfo, dry_run: bool) -> None: - ids = ( - Answer.query.filter(Answer.task_id.startswith(f"{doc.id}.")) - .with_entities(Answer.id) - .all() - ) + ids = db.session.scalars( + select(Answer.id).filter(Answer.task_id.startswith(f"{doc.id}.")) + ).all() + cnt = len(ids) delete_answers_with_ids(ids) click.echo(f"Total {cnt}") @@ -101,12 +105,12 @@ def clear( verbose: bool, ) -> None: tasks_to_delete = [f"{doc.id}.{t}" for t in task] - q = Answer.query.filter(Answer.task_id.in_(tasks_to_delete)) + stmt = select(Answer.id).filter(Answer.task_id.in_(tasks_to_delete)) if answer_from: - q = q.filter(Answer.answered_on >= answer_from) + stmt = stmt.filter(Answer.answered_on >= answer_from) if answer_to: - q = q.filter(Answer.answered_on <= answer_to) - ids = q.with_entities(Answer.id).all() + stmt = stmt.filter(Answer.answered_on <= answer_to) + ids = db.session.scalars(stmt).all() cnt = len(ids) result = delete_answers_with_ids(ids, verbose) click.echo(f"Total {cnt}") @@ -118,28 +122,49 @@ def delete_answers_with_ids( ) -> AnswerDeleteResult: if not isinstance(ids, list): raise TypeError("ids should be a list of answer ids") - d_ua = UserAnswer.query.filter(UserAnswer.answer_id.in_(ids)).delete( - synchronize_session=False - ) - d_as = AnswerSaver.query.filter(AnswerSaver.answer_id.in_(ids)).delete( - synchronize_session=False - ) - anns = Annotation.query.filter(Annotation.answer_id.in_(ids)) - d_acs = AnnotationComment.query.filter( - AnnotationComment.annotation_id.in_(anns.with_entities(Annotation.id)) - ).delete(synchronize_session=False) - d_anns = anns.delete(synchronize_session=False) - ans_items = Answer.query.filter(Answer.id.in_(ids)) + d_ua = db.session.scalars( + delete(UserAnswer) + .where(UserAnswer.answer_id.in_(ids)) + .returning(UserAnswer.id) + .execution_options(synchronize_session=False) + ).all() + d_as = db.session.scalars( + delete(AnswerSaver) + .where(AnswerSaver.answer_id.in_(ids)) + .returning(AnswerSaver.id) + .execution_options(synchronize_session=False) + ).all() + anns_stmt = select(Annotation.id).filter(Annotation.answer_id.in_(ids)) + d_acs = db.session.scalars( + delete(AnnotationComment) + .where( + AnnotationComment.annotation_id.in_(anns_stmt.with_entities(Annotation.id)) + ) + .returning(AnnotationComment.id) + .execution_options(synchronize_session=False) + ).all() + d_anns = db.session.scalars( + delete(Annotation) + .where(Annotation.id.in_(anns_stmt)) + .returning(Annotation.id) + .execution_options(synchronize_session=False) + ).all() + ans_items_stmt = select(Answer).filter(Answer.id.in_(ids)) if verbose: click.echo( "\n".join( [ f"taskid: {a.task_id}, points: {a.points}, answered_on: {a.answered_on}; saver: {a.saver}" - for a in ans_items + for a in db.session.scalars(ans_items_stmt) ] ) ) - d_ans = ans_items.delete(synchronize_session=False) + d_ans = db.session.scalars( + delete(Answer) + .where(Answer.id.in_(ans_items_stmt.with_only_columns([Answer.id]))) + .returning(Answer.id) + .execution_options(synchronize_session=False) + ).all() return AnswerDeleteResult( useranswer=d_ua, answersaver=d_as, @@ -158,15 +183,14 @@ def delete_answers_with_ids( def revalidate( doc: DocInfo, deadline: datetime, group: str, dry_run: bool, may_invalidate: bool ) -> None: - answers: list[tuple[Answer, str]] = ( - Answer.query.filter(Answer.task_id.startswith(f"{doc.id}.")) + answers: list[tuple[Answer, str]] = db.session.scalars( + select(Answer, User.name) .join(User, Answer.users) .join(UserGroup, User.groups) - .filter(UserGroup.name == group) + .filter(Answer.task_id.startswith(f"{doc.id}.")) .order_by(Answer.answered_on.desc()) - .with_entities(Answer, User.name) - .all() - ) + ).all() + changed_to_valid = 0 changed_to_invalid = 0 for a, name in answers: @@ -199,13 +223,13 @@ def truncate_large(doc: DocInfo, limit: int, to: int, dry_run: bool) -> None: if limit < to: click.echo("limit must be >= to") sys.exit(1) - q = Answer.query.filter(Answer.task_id.startswith(f"{doc.id}.")) - total = q.count() - anss: list[Answer] = ( - q.filter(func.length(Answer.content) > limit) - .options(joinedload(Answer.users_all)) - .all() - ) + stmt = select(Answer).filter(Answer.task_id.startswith(f"{doc.id}.")) + total = db.session.scalar(stmt.with_only_columns([func.count()])) + anss: list[Answer] = db.session.scalars( + stmt.filter(func.length(Answer.content) > limit).options( + joinedload(Answer.users_all) + ) + ).all() note = " (answer truncated)" try_keys = ["usercode", "c", "userinput"] truncated = 0 @@ -243,13 +267,13 @@ def truncate_large(doc: DocInfo, limit: int, to: int, dry_run: bool) -> None: def compress_uploads(item: Item, dry_run: bool) -> None: docs = collect_docs(item) for d in docs: - uploads: list[Block] = ( - Answer.query.filter(Answer.task_id.startswith(f"{d.id}.")) + uploads: list[Block] = db.session.scalars( + select(Block) + .select_from(Answer) + .filter(Answer.task_id.startswith(f"{d.id}.")) .join(AnswerUpload) .join(Block) - .with_entities(Block) - .all() - ) + ).all() for u in uploads: path = u.description if path.lower().endswith(".pdf"): diff --git a/timApp/admin/associate_old_uploads.py b/timApp/admin/associate_old_uploads.py index 49b12c9435..0b55052c72 100644 --- a/timApp/admin/associate_old_uploads.py +++ b/timApp/admin/associate_old_uploads.py @@ -1,4 +1,6 @@ -from typing import Optional, Callable +from typing import Callable + +from sqlalchemy import select from timApp.admin.search_in_documents import ( SearchArgumentsBasic, @@ -42,10 +44,13 @@ def del_anon(u: UploadedFile) -> None: SearchArgumentsBasic(format="", onlyfirst=False, regex=True, term=r), del_anon, ) - orphans = Block.query.filter( - Block.type_id.in_([BlockType.File.value, BlockType.Image.value]) - & Block.id.notin_(BlockAssociation.query.with_entities(BlockAssociation.child)) - ).all() + orphans = db.session.execute( + select(Block) + .filter( + Block.type_id.in_([BlockType.File.value, BlockType.Image.value]) + & Block.id.notin_(select(BlockAssociation.child)) + ) + ).scalars().all() print(f"Deleting anon accesses from {len(orphans)} orphan uploads") for o in orphans: del_anon(UploadedFile(o)) diff --git a/timApp/admin/change_group_email.py b/timApp/admin/change_group_email.py index 23947cb940..1d75574701 100644 --- a/timApp/admin/change_group_email.py +++ b/timApp/admin/change_group_email.py @@ -1,3 +1,5 @@ +from sqlalchemy import select + from timApp.timdb.dbaccess import get_files_path from timApp.timdb.sqa import db from timApp.timdb.timdb import TimDb @@ -12,7 +14,10 @@ def change_email() -> None: while True: # groupname = input("Input group to edit: ") groupname = "mallikurssinryhma1" - group = UserGroup.query.filter_by(name="mallikurssinryhma1").first() + group = db.session.scalars( + select(UserGroup).filter_by(name=groupname) + .limit(1) + ).first() users: list[User] = group.users new_email = input("Input new email suffix: ") print("New values:") diff --git a/timApp/admin/fix_imagex_freehanddata.py b/timApp/admin/fix_imagex_freehanddata.py index 21e5d17f30..dd4eb42119 100644 --- a/timApp/admin/fix_imagex_freehanddata.py +++ b/timApp/admin/fix_imagex_freehanddata.py @@ -1,8 +1,11 @@ import json +from sqlalchemy import select + from timApp.admin.util import process_items, create_argparser, DryrunnableArguments from timApp.answer.answer import Answer from timApp.document.docinfo import DocInfo +from timApp.timdb.sqa import db def fix_imagex_freehanddata(doc: DocInfo, args: DryrunnableArguments) -> int: @@ -15,8 +18,11 @@ def fix_imagex_freehanddata(doc: DocInfo, args: DryrunnableArguments) -> int: :param args: The arguments. """ errors = 0 - answers: list[Answer] = Answer.query.filter( - Answer.task_id.startswith(f"{doc.id}.") + answers: list[Answer] = db.session.scalars( + select(Answer) + .filter( + Answer.task_id.startswith(f"{doc.id}.") + ) ).all() for a in answers: data = a.content_as_json diff --git a/timApp/admin/fix_orphan_documents.py b/timApp/admin/fix_orphan_documents.py index 2fe5bc5aaf..a5fbcdd00c 100644 --- a/timApp/admin/fix_orphan_documents.py +++ b/timApp/admin/fix_orphan_documents.py @@ -2,6 +2,8 @@ import shutil from os.path import isfile +from sqlalchemy import select + from timApp.document.docentry import DocEntry from timApp.document.translation.translation import Translation from timApp.folder.folder import Folder @@ -16,10 +18,12 @@ def fix_orphans_without_docentry() -> None: creates a DocEntry for them under 'orphans' directory.""" orphan_folder_title = "orphans" f = Folder.create("orphans", UserGroup.get_admin_group()) - orphans: list[Block] = Block.query.filter( - (Block.type_id == 0) - & Block.id.notin_(DocEntry.query.with_entities(DocEntry.id)) - & Block.id.notin_(Translation.query.with_entities(Translation.doc_id)) + orphans: list[Block] = db.session.scalars( + select(Block).filter( + (Block.type_id == 0) + & Block.id.notin_(select(DocEntry.id)) + & Block.id.notin_(select(Translation.doc_id)) + ) ).all() for o in orphans: @@ -39,9 +43,9 @@ def move_docs_without_block(dry_run: bool) -> None: doc_folders = [f for f in os.listdir(docs_folder) if not isfile(f)] existing_blocks = { str(i) - for i, in Block.query.filter_by(type_id=BlockType.Document.value) - .with_entities(Block.id) - .all() + for i, in db.session.scalars( + select(Block.id).filter_by(type_id=BlockType.Document.value) + ).all() } docs_orphans = os.path.join(files_root, "orphans", "docs") pars_orphans = os.path.join(files_root, "orphans", "pars") diff --git a/timApp/admin/item_cli.py b/timApp/admin/item_cli.py index ce8d74596d..2f5a6bca2e 100644 --- a/timApp/admin/item_cli.py +++ b/timApp/admin/item_cli.py @@ -3,6 +3,7 @@ import click from flask.cli import AppGroup +from sqlalchemy import select, delete from timApp.admin.fix_orphan_documents import ( fix_orphans_without_docentry, @@ -25,16 +26,18 @@ @item_cli.command("cleanup_default_rights_names") def cleanup_default_right_doc_names() -> None: - bs: list[Block] = Block.query.filter( - Block.description.in_( - [ - "templates/DefaultDocumentRights", - "templates/DefaultFolderRights", - "$DefaultFolderRights", - "$DefaultDocumentRights", - ] + bs: list[Block] = db.session.scalars( + select(Block).filter( + Block.description.in_( + [ + "templates/DefaultDocumentRights", + "templates/DefaultFolderRights", + "$DefaultFolderRights", + "$DefaultDocumentRights", + ] + ) + & (Block.type_id == BlockType.Document.value) ) - & (Block.type_id == BlockType.Document.value) ).all() num_changed = len(bs) for b in bs: @@ -50,8 +53,8 @@ def cleanup_default_right_doc_names() -> None: def cleanup_bookmark_docs( dry_run: bool, prompt_before_commit: bool, max_docs: int | None ) -> None: - new_bookmark_users: list[User] = User.query.filter( - User.prefs.contains('"bookmarks":') + new_bookmark_users: list[User] = db.session.scalars( + select(User).filter(User.prefs.contains('"bookmarks":')) ).all() docs_to_delete = set() processed_users = 0 @@ -80,11 +83,14 @@ def cleanup_bookmark_docs( click.echo(f"Deleting unused bookmarks document of {u}") block = bm_doc.block block.accesses = {} - Translation.query.filter_by(doc_id=bm_doc.id).delete() - ReadParagraph.query.filter_by(doc_id=bm_doc.id).delete() - PendingNotification.query.filter_by(doc_id=bm_doc.id).delete() - VelpGroupsInDocument.query.filter_by(doc_id=bm_doc.id).delete() - Notification.query.filter_by(block_id=bm_doc.id).delete() + for t in ( + Translation, + ReadParagraph, + PendingNotification, + VelpGroupsInDocument, + Notification, + ): + db.session.execute(delete(t).where(t.doc_id == bm_doc.id)) db.session.delete(bm_doc) db.session.delete(block) if dry_run: diff --git a/timApp/admin/language_cli.py b/timApp/admin/language_cli.py index 02c2e15e42..08ac706df9 100644 --- a/timApp/admin/language_cli.py +++ b/timApp/admin/language_cli.py @@ -15,6 +15,7 @@ import click import langcodes from flask.cli import AppGroup +from sqlalchemy import select from timApp.document.translation.language import Language from timApp.tim_app import app @@ -34,7 +35,9 @@ def remove(lang_code: str) -> None: :return: None """ - exists = Language.query.filter(lang_code == Language.lang_code).first() + exists: Language | None = db.session.scalars( + select(Language).filter(lang_code == Language.lang_code).limit(1) + ).first() if exists: if click.confirm("This action cannot be reversed. Continue?"): click.echo( @@ -64,7 +67,9 @@ def add(lang_name: str) -> None: click.echo(f"Failed to create language: {str(e)}") return - exists = Language.query.filter(lang.lang_code == Language.lang_code).first() + exists: Language | None = db.session.scalars( + select(Language).filter(lang.lang_code == Language.lang_code).limit(1) + ).first() if exists: click.echo(f"Language code '{lang.lang_code}' already exists in the database.") else: @@ -94,7 +99,7 @@ def add_all_supported_languages(log: bool = False) -> None: :return: None. """ # Add to the database the languages found in config and skip existing ones. - langset = {x[0] for x in Language.query.with_entities(Language.lang_code).all()} + langset = {x[0] for x in db.session.scalars(select(Language.lang_code))} for l in app.config["LANGUAGES"]: if type(l) is dict: lang = Language( @@ -156,7 +161,9 @@ def create(langcode: str, langname: str, autonym: str, flag_uri: str) -> None: click.echo(f"Failed to create new language: {str(e)}") return - exists = Language.query.filter(standard_code == Language.lang_code).first() + exists = db.session.scalars( + select(Language).filter(standard_code == Language.lang_code).limit(1) + ).first() if exists: click.echo(f"Language code '{standard_code}' already exists in the database.") else: diff --git a/timApp/admin/routes.py b/timApp/admin/routes.py index 607aa27e01..d89eab7428 100644 --- a/timApp/admin/routes.py +++ b/timApp/admin/routes.py @@ -1,6 +1,7 @@ import os from flask import flash, url_for, Response +from sqlalchemy import select from timApp.auth.accesshelper import verify_admin from timApp.timdb.sqa import db @@ -45,13 +46,13 @@ def restart_server() -> Response: @admin_bp.get("/users/search/") def search_users(term: str, full: bool = False) -> Response: verify_admin() - result = ( - User.query.filter( + result: list[User] = db.session.scalars( + select(User) + .filter( User.name.ilike(f"%{term}%") | User.real_name.ilike(f"%{term}%") | User.email.ilike(f"%{term}%") ) .order_by(User.id) - .all() - ) + ).all() return json_response([u.to_json(contacts=True, full=full) for u in result]) diff --git a/timApp/admin/translationservice_cli.py b/timApp/admin/translationservice_cli.py index fc5563edab..4c7cf4a0c1 100644 --- a/timApp/admin/translationservice_cli.py +++ b/timApp/admin/translationservice_cli.py @@ -14,6 +14,7 @@ import click from flask.cli import AppGroup +from sqlalchemy import select from timApp.document.translation.translator import TranslationService from timApp.tim_app import app @@ -51,10 +52,7 @@ def add_all_tr_services_to_session(log: bool = False) -> None: :return: None. """ existing_services = { - x[0] - for x in TranslationService.query.with_entities( - TranslationService.service_name - ).all() + x[0] for x in db.session.scalars(select(TranslationService.service_name)) } for translator, init_data in app.config["MACHINE_TRANSLATORS"]: service_name = translator.__mapper_args__["polymorphic_identity"] diff --git a/timApp/admin/user_cli.py b/timApp/admin/user_cli.py index cafea9e450..8dec39635d 100644 --- a/timApp/admin/user_cli.py +++ b/timApp/admin/user_cli.py @@ -11,13 +11,12 @@ import click from flask import current_app from flask.cli import AppGroup -from sqlalchemy import func +from sqlalchemy import func, select from timApp.admin.import_accounts import import_accounts_impl from timApp.auth.accesstype import AccessType from timApp.document.docentry import DocEntry from timApp.document.docinfo import move_document -from tim_common.timjsonencoder import TimJsonEncoder from timApp.tim_app import get_home_organization_group from timApp.timdb.sqa import db from timApp.user.personaluniquecode import SchacPersonalUniqueCode, PersonalUniqueCode @@ -28,6 +27,7 @@ from timApp.user.userutils import check_password_hash from timApp.util.flask.requesthelper import RouteException, NotExist from timApp.util.utils import approximate_real_name +from tim_common.timjsonencoder import TimJsonEncoder def create_user_info_set(u: User) -> set[str]: @@ -93,7 +93,9 @@ def migrate_themes_to_styles(dry_run: bool, skip_warnings: bool) -> None: click.echo("Updating user styles") - for u in User.query.filter(User.prefs != None): # type: User + for u in db.session.scalars( + select(User).filter(User.prefs != None) + ): # type: User prefs_json: dict = json.loads(u.prefs) css_combined = prefs_json.pop("css_combined", None) css_files: dict[str, bool] = prefs_json.pop("css_files", {}) @@ -334,7 +336,7 @@ def create( ) -> None: """Creates or updates a user.""" - user = User.query.filter_by(name=username).first() + user = db.session.scalars(select(User).filter_by(name=username).limit(1)).first() info = UserInfo( username=username, email=email or None, @@ -388,7 +390,9 @@ def create_mass_users( return for i in range(lowerlimit, higherlimit + 1): strnum = str(i) - user = User.query.filter_by(name=username + strnum).first() + user = db.session.scalars( + select(User).filter_by(name=username + strnum).limit(1) + ).first() # print(i) info = UserInfo( username=username + strnum, @@ -409,14 +413,12 @@ def create_mass_users( @user_cli.command() def fix_aalto_student_ids() -> None: - users_to_fix: list[User] = ( - UserGroup.query.filter( - UserGroup.name.in_(["aalto19test", "cs-a1141-2017-2018"]) - ) + users_to_fix: list[User] = db.session.scalars( + select(User) + .select_from(UserGroup) + .filter(UserGroup.name.in_(["aalto19test", "cs-a1141-2017-2018"])) .join(User, UserGroup.users) - .with_entities(User) - .all() - ) + ).all() for u in users_to_fix: u.set_unique_codes( [ @@ -549,18 +551,15 @@ def find_duplicate_accounts() -> None: def find_duplicate_accounts_by_email() -> list[tuple[User, set[User]]]: email_lwr = func.lower(User.email) - dupes: list[User] = ( - User.query.filter( + dupes: list[User] = db.session.scalars( + select(User) + .filter( email_lwr.in_( - User.query.group_by(email_lwr) - .having(func.count("*") > 1) - .with_entities(email_lwr) - .all() + select(email_lwr).group_by(email_lwr).having(func.count("*") > 1) ) ) .order_by(User.email) - .all() - ) + ).all() result = [] dupegroups = [ list(g) for _, g in (itertools.groupby(dupes, lambda u: u.email.lower())) diff --git a/timApp/answer/answers.py b/timApp/answer/answers.py index 74beea80dc..d2daacc508 100644 --- a/timApp/answer/answers.py +++ b/timApp/answer/answers.py @@ -20,9 +20,10 @@ # noinspection PyUnresolvedReferences from bs4 import UnicodeDammit from flask import current_app -from sqlalchemy import func, Numeric, Float, true, case +from sqlalchemy import func, Numeric, Float, true, case, select from sqlalchemy.dialects.postgresql import aggregate_order_by -from sqlalchemy.orm import selectinload, defaultload, Query, joinedload, contains_eager +from sqlalchemy.orm import selectinload, defaultload, joinedload, contains_eager +from sqlalchemy.sql import Select, Subquery from timApp.answer.answer import Answer from timApp.answer.answer_models import AnswerTag, UserAnswer @@ -48,41 +49,46 @@ class ExistingAnswersInfo: count: int -def get_answers_query(task_id: TaskId, users: list[User], only_valid: bool) -> Query: - q = Answer.query.filter_by(task_id=task_id.doc_task) +def get_answers_query(task_id: TaskId, users: list[User], only_valid: bool) -> Select: + stmt = select(Answer).filter_by(task_id=task_id.doc_task) if only_valid: - q = q.filter_by(valid=True) + stmt = stmt.filter_by(valid=True) if not task_id.is_global: - q = ( - q.join(User, Answer.users) - .filter(User.id.in_([u.id for u in users])) - .group_by(Answer.id) - .with_entities(Answer.id) - .having( - (func.array_agg(aggregate_order_by(User.id, User.id))) - == sorted(u.id for u in users) + stmt = select(Answer).filter( + Answer.id.in_( + stmt.join(User, Answer.users) + .filter(User.id.in_([u.id for u in users])) + .group_by(Answer.id) + .with_only_columns([Answer.id]) + .having( + (func.array_agg(aggregate_order_by(User.id, User.id))) + == sorted(u.id for u in users) + ) + .subquery() ) - ).subquery() - q = Answer.query.filter(Answer.id.in_(q)) - q = q.order_by(Answer.id.desc()) - return q + ) + stmt = stmt.order_by(Answer.id.desc()) + return stmt def get_latest_answers_query( task_id: TaskId, users: list[User], only_valid: bool -) -> Query: - q = Answer.query.filter_by(task_id=task_id.doc_task) +) -> Select: + stmt = select(Answer).filter_by(task_id=task_id.doc_task) if only_valid: - q = q.filter_by(valid=True) - sq = ( - q.join(User, Answer.users) + stmt = stmt.filter_by(valid=True) + stmt_sub = ( + stmt.join(User, Answer.users) .filter(User.id.in_([u.id for u in users])) .group_by(User.id) .with_entities(func.max(Answer.id).label("aid"), User.id.label("uid")) .subquery() ) - datas = Answer.query.join(sq, Answer.id == sq.c.aid).with_entities(Answer) - return datas + return ( + select(Answer) + .join(stmt_sub, Answer.id == stmt_sub.c.aid) + .with_only_columns(Answer) + ) def is_redundant_answer( @@ -284,7 +290,7 @@ def get_all_answers( if options.period_from is None or options.period_to is None: raise ValueError("Answer period must be specified.") - q = get_all_answer_initial_query( + stmt = get_all_answer_initial_query( options.period_from, options.period_to, task_ids, @@ -293,10 +299,10 @@ def get_all_answers( options.include_inactive_memberships, ) - q = q.options(defaultload(Answer.users).lazyload(User.groups)) + stmt = stmt.options(defaultload(Answer.users).lazyload(User.groups)) if options.consent is not None: - q = q.filter_by(consent=options.consent) + stmt = stmt.filter_by(consent=options.consent) match options.age: case AgeOptions.MIN: @@ -309,19 +315,23 @@ def get_all_answers( minmax = Answer.id.label("minmax") counts = Answer.valid.label("count") - q = q.add_columns(minmax, counts) + # stmt = stmt.add_columns(minmax, counts) if options.age != AgeOptions.ALL: - q = q.group_by(Answer.task_id, User.id) - q = q.with_entities(minmax, counts) - sub = q.subquery() - q = Answer.query.join(sub, Answer.id == sub.c.minmax).join(User, Answer.users) - q = q.outerjoin(PluginType).options(contains_eager(Answer.plugin_type)) + stmt = stmt.group_by(Answer.task_id, User.id) + stmt = stmt.with_only_columns(minmax, counts) + sub_stmt: Subquery = stmt.subquery() + stmt = ( + select(Answer) + .join(sub_stmt, Answer.id == sub_stmt.c.minmax) + .join(User, Answer.users) + ) + stmt = stmt.outerjoin(PluginType).options(contains_eager(Answer.plugin_type)) match options.sort: case SortOptions.USERNAME: - q = q.order_by(User.name, Answer.task_id, Answer.answered_on) + stmt = stmt.order_by(User.name, Answer.task_id, Answer.answered_on) case SortOptions.TASK: - q = q.order_by(Answer.task_id, User.name, Answer.answered_on) - q = q.with_entities(Answer, User, sub.c.count) + stmt = stmt.order_by(Answer.task_id, User.name, Answer.answered_on) + stmt = stmt.with_only_columns(Answer, User, sub_stmt.c.count) result = [] result_json = [] @@ -329,7 +339,7 @@ def get_all_answers( if options.print == AnswerPrintOptions.ANSWERS_NO_LINE: lf = "" - qq: Iterable[tuple[Answer, User, int]] = q + qq: Iterable[tuple[Answer, User, int]] = db.session.execute(stmt) cnt = 0 hidden_user_names: dict[str, str] = {} @@ -471,31 +481,33 @@ def get_all_answer_initial_query( valid: ValidityOptions, groups: list[str] | None = None, include_expired_members: bool = False, -) -> Query: - q = Answer.query.filter( - (period_from <= Answer.answered_on) & (Answer.answered_on < period_to) - ).filter(Answer.task_id.in_(task_ids_to_strlist(task_ids))) +) -> Select: + stmt = ( + select(Answer) + .filter((period_from <= Answer.answered_on) & (Answer.answered_on < period_to)) + .filter(Answer.task_id.in_(task_ids_to_strlist(task_ids))) + ) match valid: case ValidityOptions.ALL: pass case ValidityOptions.INVALID: - q = q.filter_by(valid=False) + stmt = stmt.filter_by(valid=False) case ValidityOptions.VALID: - q = q.filter_by(valid=True) - q = q.join(User, Answer.users) + stmt = stmt.filter_by(valid=True) + stmt = stmt.join(User, Answer.users) if groups: - q = q.join( + stmt = stmt.join( UserGroup, User.groups_dyn if include_expired_members else User.groups ).filter(UserGroup.name.in_(groups)) - return q + return stmt def get_existing_answers_info( users: list[User], task_id: TaskId, only_valid: bool ) -> ExistingAnswersInfo: - q = get_answers_query(task_id, users, only_valid) - latest = q.first() - count = q.count() + stmt = get_answers_query(task_id, users, only_valid) + latest = db.session.scalars(stmt.limit(1)).first() + count = db.session.scalar(stmt.with_only_columns([func.count()])) return ExistingAnswersInfo(latest_answer=latest, count=count) @@ -515,11 +527,11 @@ def get_existing_answers_info( } -def valid_answers_query(task_ids: list[TaskId], valid: bool | None = True) -> Query: - return Answer.query.filter(valid_taskid_filter(task_ids, valid)) +def valid_answers_query(task_ids: list[TaskId], valid: bool | None = True) -> Select: + return select(Answer).filter(valid_taskid_filter(task_ids, valid)) -def valid_taskid_filter(task_ids: list[TaskId], valid: bool | None = True) -> Query: +def valid_taskid_filter(task_ids: list[TaskId], valid: bool | None = True) -> Any: res = Answer.task_id.in_(task_ids_to_strlist(task_ids)) if valid is not None: res = res & (Answer.valid == valid) @@ -552,15 +564,16 @@ def get_users_for_tasks( return [] subquery_annotantions = ( - Annotation.query.filter_by(valid_until=None) + select(Annotation) + .filter_by(valid_until=None) .group_by(Annotation.answer_id) - .with_entities( + .with_only_columns( Annotation.answer_id.label("annotation_answer_id"), func.sum(Annotation.points).label("velp_points"), ) .subquery() ) - subquery_answers = Answer.query.with_entities( + subquery_answers = select( Answer.id, Answer.points, Answer.answered_on, Answer.valid ).subquery() if answer_filter is None: @@ -578,7 +591,7 @@ def get_users_for_tasks( .filter(answer_filter) .join(UserAnswer, UserAnswer.answer_id == Answer.id) .group_by(UserAnswer.user_id, Answer.task_id) - .with_entities( + .with_only_columns( Answer.task_id, UserAnswer.user_id.label("uid"), func.max(Answer.id).filter(Answer.valid == True).label("aid_valid"), @@ -589,7 +602,7 @@ def get_users_for_tasks( ) sub_joined = ( - db.session.query(subquery_user_answers, subquery_answers, subquery_annotantions) + select(subquery_user_answers, subquery_answers, subquery_annotantions) .outerjoin( subquery_answers, # Pick the latest valid answer. @@ -610,19 +623,23 @@ def get_users_for_tasks( ) .subquery() ) - main = User.query.join(UserAnswer, UserAnswer.user_id == User.id).join( - sub_joined, - ( + main_stmt = ( + select(User) + .join(UserAnswer, UserAnswer.user_id == User.id) + .join( + sub_joined, ( - (sub_joined.c.aid_valid != None) - & (sub_joined.c.aid_valid == UserAnswer.answer_id) - ) - | ( - (sub_joined.c.aid_valid == None) - & (sub_joined.c.aid_any == UserAnswer.answer_id) + ( + (sub_joined.c.aid_valid != None) + & (sub_joined.c.aid_valid == UserAnswer.answer_id) + ) + | ( + (sub_joined.c.aid_valid == None) + & (sub_joined.c.aid_any == UserAnswer.answer_id) + ) ) + & (User.id == sub_joined.c.uid), ) - & (User.id == sub_joined.c.uid), ) group_by_cols = [] cols = [] @@ -635,14 +652,14 @@ def get_users_for_tasks( group_by_cols.append(doc_id) cols.append(doc_id) if user_ids is not None: - main = main.filter(User.id.in_(user_ids)) + main_stmt = main_stmt.filter(User.id.in_(user_ids)) if current_app.config["LOAD_STUDENT_IDS_IN_TEACHER"]: - main = main.options(joinedload("uniquecodes")) - main = main.group_by(User.id, *group_by_cols) + main_stmt = main_stmt.options(joinedload("uniquecodes")) + main_stmt = main_stmt.group_by(User.id, *group_by_cols) # prevents error: # column "usergroup_1.id" must appear in the GROUP BY clause or be used in an aggregate function - main = main.options(selectinload(User.groups)) + main_stmt = main_stmt.options(selectinload(User.groups)) task_sum = ( func.round( func.sum( @@ -676,7 +693,7 @@ def get_users_for_tasks( else: time_cols = [] - main = main.with_entities( + main_stmt = main_stmt.with_only_columns( User, func.count(sub_joined.c.task_id).label("task_count"), task_sum, @@ -690,7 +707,7 @@ def get_users_for_tasks( ).order_by(User.real_name) def g() -> Generator[UserTaskEntry, None, None]: - for r in main: + for r in db.session.execute(main_stmt): d = r._asdict() d["user"] = d.pop("User") task = d["task_points"] @@ -1089,16 +1106,17 @@ def add_missing_users_from_groups(result: list, usergroups: list[UserGroup]) -> def get_global_answers(parsed_task_ids: dict[str, TaskId]) -> list[Answer]: sq2 = ( - Answer.query.filter( + select(Answer) + .filter( Answer.task_id.in_( [tid.doc_task for tid in parsed_task_ids.values() if tid.is_global] ) ) .group_by(Answer.task_id) - .with_entities(func.max(Answer.id).label("aid")) + .with_only_columns(func.max(Answer.id).label("aid")) .subquery() ) global_datas = ( - Answer.query.join(sq2, Answer.id == sq2.c.aid).with_entities(Answer).all() + select(Answer).join(sq2, Answer.id == sq2.c.aid).with_only_columns(Answer).all() ) - return global_datas + return db.session.scalars(global_datas).all() diff --git a/timApp/answer/backup.py b/timApp/answer/backup.py index 7b33637e00..1dc97680cb 100644 --- a/timApp/answer/backup.py +++ b/timApp/answer/backup.py @@ -2,6 +2,7 @@ import filelock from flask import current_app, Response +from sqlalchemy import select from timApp.answer.answer import Answer from timApp.answer.exportedanswer import ExportedAnswer @@ -69,12 +70,15 @@ def sync_user_group_memberships_if_enabled(user: User) -> None: user_groups: list[str] = [ ugn for ugn, in ( - db.session.query(UserGroup.name) - .join( - UserGroupMember, - (UserGroup.id == UserGroupMember.usergroup_id) & membership_current, + db.session.execute( + select(UserGroup.name) + .join( + UserGroupMember, + (UserGroup.id == UserGroupMember.usergroup_id) & membership_current, + ) + .filter(UserGroupMember.user_id == user.id) ) - .filter(UserGroupMember.user_id == user.id) + .scalars() .all() ) ] diff --git a/timApp/answer/routes.py b/timApp/answer/routes.py index b840029c92..37ddd39667 100644 --- a/timApp/answer/routes.py +++ b/timApp/answer/routes.py @@ -10,8 +10,8 @@ from flask import current_app from flask import request from marshmallow.utils import missing -from sqlalchemy import func -from sqlalchemy.orm import lazyload, joinedload +from sqlalchemy import func, select +from sqlalchemy.orm import lazyload, joinedload, selectinload from werkzeug.exceptions import NotFound from timApp.answer.answer import Answer, AnswerData @@ -173,7 +173,6 @@ None, # Clear points, only by teacher ] - # TODO: loggable route (points in url?) @answers.put("/saveReview//") def save_review_points( @@ -187,11 +186,15 @@ def save_review_points( verify_view_access(doc) if not is_peerreview_enabled(doc): raise AccessDenied("Peer review is not enabled") - peer_review = PeerReview.query.filter_by( - block_id=tid.doc_id, - task_name=tid.task_name, - reviewer_id=curr_user_id, - reviewable_id=user_id, + peer_review = db.session.scalars( + select(PeerReview) + .filter_by( + block_id=tid.doc_id, + task_name=tid.task_name, + reviewer_id=curr_user_id, + reviewable_id=user_id, + ) + .limit(1) ).first() if not peer_review: raise RouteException("Invalid review target") @@ -226,7 +229,7 @@ def save_points(answer_id: int, user_id: int, points: PointsType = None) -> Resp ) except PluginException as e: raise RouteException(str(e)) - a = Answer.query.get(answer_id) + a = db.session.get(Answer, answer_id) try: points = points_to_float(points) except ValueError: @@ -439,11 +442,10 @@ def get_useranswers_for_task( .group_by(Answer.task_id) .subquery() ) - answs: list[Answer] = ( - Answer.query.join(sub, Answer.id == sub.c.col) - .options(joinedload(Answer.users_all)) - .all() - ) + answs: list[Answer] = db.scalars( + select(Answer).join(sub, Answer.id == sub.c.col) + .options(selectinload(Answer.users_all)) + ).all() for answer in answs: asd = answer.to_json() asd.pop("points", None) @@ -457,16 +459,17 @@ def get_globals_for_tasks(task_ids: list[TaskId], answer_map: dict[str, dict]) - sub = ( valid_answers_query(task_ids) .add_columns(col, cnt) - .with_entities(col, cnt) + .with_only_columns(col, cnt) .group_by(Answer.task_id) .subquery() ) answers_all: list[tuple[Answer, int]] = ( - Answer.query.join(sub, Answer.id == sub.c.col) - .with_entities(Answer, sub.c.cnt) + select(Answer) + .join(sub, Answer.id == sub.c.col) + .with_only_columns(Answer, sub.c.cnt) .all() ) - for answer, _ in answers_all: + for answer, _ in db.session.scalars(answers_all): asd = answer.to_json() answer_map[answer.task_id] = asd @@ -751,7 +754,7 @@ def post_answer_impl( user_id = answer_browser_data.get("userId", None) if answer_id is not None: - answer = Answer.query.get(answer_id) + answer = db.session.get(Answer, answer_id) if not answer: raise PluginException(f"Answer not found: {answer_id}") expected_task_id = answer.task_id @@ -777,7 +780,7 @@ def post_answer_impl( "Permission denied: you are not in teachers group." ) if user_id: - ctx_user = User.query.get(user_id) + ctx_user = db.session.get(User, user_id) if not ctx_user: raise PluginException(f"User {user_id} not found") users = [ctx_user] # TODO: Vesa's hack to save answer to student @@ -859,7 +862,7 @@ def post_answer_impl( # TODO: Stack gets default for the field there??? answer_id = answer_browser_data.get("answer_id", None) if answer_id is not None and curr_user.logged_in: - answer = Answer.query.get(answer_id) + answer = db.session.get(Answer, answer_id) if answer: state = try_load_json(answer.content) @@ -1280,8 +1283,10 @@ def check_answerupload_file_accesses( """ uploads: list[AnswerUpload] = [] doc_map = {} - blocks = Block.query.filter( - Block.description.in_(filelist) & (Block.type_id == BlockType.Upload.value) + blocks = db.session.scalars( + select(Block).filter( + Block.description.in_(filelist) & (Block.type_id == BlockType.Upload.value) + ) ).all() if len(blocks) != len(filelist): block_filelist = [b.description for b in blocks] @@ -1569,12 +1574,12 @@ def export_answers(doc_path: str) -> Response: if not d: raise RouteException("Document not found") verify_teacher_access(d) - answer_list: list[tuple[Answer, str]] = ( - Answer.query.filter(Answer.task_id.startswith(f"{d.id}.")) + answer_list: list[tuple[Answer, str]] = db.session.scalars( + select(Answer) + .filter(Answer.task_id.startswith(f"{d.id}.")) .join(User, Answer.users) - .with_entities(Answer, User.email) - .all() - ) + .with_only_columns(Answer, User.email) + ).all() return json_response( [ { @@ -1608,7 +1613,9 @@ def import_answers( raise NotFound(f"No group with name '{group}'") verify_group_view_access(ug) doc_paths = {doc_map.get(a.doc, a.doc) for a in exported_answers} - docs = DocEntry.query.filter(DocEntry.name.in_(doc_paths)).all() + docs = db.session.scalars( + select(DocEntry).filter(DocEntry.name.in_(doc_paths)) + ).all() doc_path_map = {d.path: d for d in docs} missing_docs = doc_paths - set(doc_path_map) if missing_docs: @@ -1633,12 +1640,12 @@ def import_answers( f"Found: {seq_to_str([str((a.email, a.username)) for a in mixed_answers])}" ) - existing_answers: list[tuple[Answer, str]] = ( - Answer.query.filter(filter_cond) + existing_answers: list[tuple[Answer, str]] = db.session.scalars( + select(Answer) + .filter(filter_cond) .join(User, Answer.users) - .with_entities(Answer, User.name) - .all() - ) + .with_only_columns(Answer, User.name) + ).all() def convert_email_case(email: str | None) -> str | None: if email is None: @@ -1661,9 +1668,11 @@ def convert_email_case(email: str | None) -> str | None: dupes = 0 # noinspection PyUnresolvedReferences - all_users = User.query.filter( - email_field.in_([a.email for a in exported_answers if a.email]) - | name_field.in_([a.username for a in exported_answers if a.username]) + all_users = db.session.scalars( + select(User).filter( + email_field.in_([a.email for a in exported_answers if a.email]) + | name_field.in_([a.username for a in exported_answers if a.username]) + ) ).all() if not match_email_case: @@ -1779,12 +1788,12 @@ def get_answers(task_id: str, user_id: int) -> Response: if tid.is_global: verify_view_access(d) user_context = user_context_with_logged_in(curr_user) - user_answers = ( - Answer.query.filter_by(task_id=tid.doc_task) + user_answers = db.session.scalars( + select(Answer) + .filter_by(task_id=tid.doc_task) .order_by(Answer.id.desc()) .options(joinedload(Answer.users_all)) - .all() - ) + ).all() user = curr_user else: user = User.get_by_id(user_id) @@ -2073,7 +2082,7 @@ def get_state( raise RouteException("Non-existent user") view_ctx = view_ctx_with_urlmacros(ViewRoute.View, origin=get_origin_from_request()) if answer_id: - answer = Answer.query.get(answer_id) + answer = db.session.get(Answer, answer_id) if not answer: raise RouteException("Non-existent answer") tid = TaskId.parse(answer.task_id) @@ -2170,16 +2179,19 @@ def get_task_users(task_id: str, peer_review: bool = False) -> Response: users = list(r.reviewable for r in reviews if r.task_name == tid.task_name) else: usergroups = request.args.getlist("groups") - q = ( - User.query.options(lazyload(User.groups)) + stmt = ( + select(User) + .options(lazyload(User.groups)) .join(Answer, User.answers) .filter_by(task_id=task_id) .order_by(User.real_name.asc()) .distinct() ) if usergroups: - q = q.join(UserGroup, User.groups).filter(UserGroup.name.in_(usergroups)) - users = q.all() + stmt = stmt.join(UserGroup, User.groups).filter( + UserGroup.name.in_(usergroups) + ) + users = db.session.scalars(stmt).all() if hide_names_in_teacher(d): model_u = User.get_model_answer_user() for user in users: @@ -2197,12 +2209,16 @@ def rename_answers(old_name: str, new_name: str, doc_path: str) -> Response: for n in (old_name, new_name): if not re.fullmatch("[a-zA-Z0-9_-]+", n): raise RouteException(f"Invalid task name: {n}") - conflicts = Answer.query.filter_by(task_id=f"{d.id}.{new_name}").count() + conflicts = db.session.scalar( + select(func.count(Answer.id)).filter_by(task_id=f"{d.id}.{new_name}") + ) if conflicts > 0 and not force: raise RouteException( f"The new name conflicts with {conflicts} other answers with the same task name." ) - answers_to_rename = Answer.query.filter_by(task_id=f"{d.id}.{old_name}").all() + answers_to_rename = db.session.scalars( + select(Answer).filter_by(task_id=f"{d.id}.{old_name}") + ).all() for a in answers_to_rename: a.task_id = f"{d.id}.{new_name}" db.session.commit() @@ -2226,10 +2242,14 @@ def clear_task_block(user: str, task_id: str) -> Response: b = TaskBlock.get_by_task(tid.doc_task) if not b: return json_response({"cleared": False}) - ba = BlockAccess.query.filter_by( - block_id=b.id, - type=AccessType.view.value, - usergroup_id=user_obj.get_personal_group().id, + ba = db.session.scalars( + select(BlockAccess) + .filter_by( + block_id=b.id, + type=AccessType.view.value, + usergroup_id=user_obj.get_personal_group().id, + ) + .limit(1) ).first() if not ba or not ba.accessible_to: return json_response({"cleared": False}) @@ -2269,10 +2289,14 @@ def unlock_locked_task(task_id: str) -> Response: if prerequisite_info.requireLock: b = TaskBlock.get_by_task(prerequisite_taskid.doc_task) if b: - ba = BlockAccess.query.filter_by( - block_id=b.id, - type=AccessType.view.value, - usergroup_id=current_user.get_personal_group().id, + ba = db.session.scalars( + select(BlockAccess) + .filter_by( + block_id=b.id, + type=AccessType.view.value, + usergroup_id=current_user.get_personal_group().id, + ) + .limit(1) ).first() if ba and ba.accessible_to and ba.accessible_to < get_current_time(): return json_response({"unlocked": True}) @@ -2315,10 +2339,14 @@ def unlock_task(task_id: str) -> Response: if not b: b = insert_task_block(task_id=tid.doc_task, owner_groups=d.owners) else: - ba = BlockAccess.query.filter_by( - block_id=b.id, - type=AccessType.view.value, - usergroup_id=current_user.get_personal_group().id, + ba = db.session.scalars( + select(BlockAccess) + .filter_by( + block_id=b.id, + type=AccessType.view.value, + usergroup_id=current_user.get_personal_group().id, + ) + .limit(1) ).first() if not ba: time_now = get_current_time() diff --git a/timApp/auth/access/routes.py b/timApp/auth/access/routes.py index f4eb22d757..822840f522 100644 --- a/timApp/auth/access/routes.py +++ b/timApp/auth/access/routes.py @@ -5,11 +5,13 @@ from dataclasses import field from flask import Response +from sqlalchemy import select from timApp.auth.access.util import set_locked_access_type, set_locked_active_groups from timApp.auth.accesshelper import verify_logged_in, AccessDenied from timApp.auth.accesstype import AccessType from timApp.auth.sessioninfo import get_current_user_object +from timApp.timdb.sqa import db from timApp.user.groups import ( verify_group_edit_access, get_group_or_abort, @@ -69,9 +71,13 @@ def lock_active_groups(group_ids: list[int] | None) -> Response: } if not user.is_admin: - groups: list[UserGroup] = UserGroup.query.filter( - UserGroup.id.in_(group_ids_set) - ).all() + groups: list[UserGroup] = ( + db.session.execute( + select(UserGroup).filter(UserGroup.id.in_(group_ids_set)) + ) + .scalars() + .all() + ) for ug in groups: if not verify_group_edit_access(ug, user, require=False): raise AccessDenied( @@ -120,7 +126,11 @@ def find_editable_groups( verify_logged_in() user = get_current_user_object() user.bypass_access_lock = True - ugs = UserGroup.query.filter(UserGroup.id.in_(group_ids)).all() + ugs = ( + db.session.execute(select(UserGroup).filter(UserGroup.id.in_(group_ids))) + .scalars() + .all() + ) visible_ugs = [ ug for ug in ugs if user.is_admin or verify_group_edit_access(ug, require=False) ] diff --git a/timApp/auth/accesshelper.py b/timApp/auth/accesshelper.py index b60a7f408e..6fa4b10797 100644 --- a/timApp/auth/accesshelper.py +++ b/timApp/auth/accesshelper.py @@ -7,7 +7,7 @@ from flask import flash, current_app from flask import request, g from marshmallow import missing -from sqlalchemy import inspect +from sqlalchemy import inspect, select from timApp.answer.answer import Answer from timApp.auth.accesstype import AccessType @@ -287,25 +287,31 @@ def abort_if_not_access_and_required( block_ids = [block.id, *get_inherited_right_blocks(block)] if check_duration: - ba = ( - BlockAccess.query.filter(BlockAccess.block_id.in_(block_ids)) + ba = db.session.scalars( + select(BlockAccess) + .filter(BlockAccess.block_id.in_(block_ids)) .filter_by( type=access_type.value, usergroup_id=get_current_user_group(), ) - .first() - ) + .limit(1) + ).first() if ba is None: ba_group: BlockAccess = ( - BlockAccess.query.filter(BlockAccess.block_id.in_(block_ids)) - .filter_by(type=access_type.value) - .filter( - BlockAccess.usergroup_id.in_( - get_current_user_object() - .get_groups(include_expired=False) - .with_entities(UserGroup.id) + db.session.execute( + select(BlockAccess) + .filter(BlockAccess.block_id.in_(block_ids)) + .filter_by(type=access_type.value) + .filter( + BlockAccess.usergroup_id.in_( + get_current_user_object() + .get_groups(include_expired=False) + .with_only_columns(UserGroup.id) + ) ) + .limit(1) ) + .scalars() .first() ) if ba_group is not None: @@ -664,7 +670,7 @@ def verify_answer_access( required_task_access_level: TaskIdAccess = TaskIdAccess.ReadOnly, allow_grace_period: bool = False, ) -> tuple[Answer, int]: - answer: Answer = Answer.query.get(answer_id) + answer: Answer = db.session.get(Answer, answer_id) if answer is None: raise RouteException("Non-existent answer") tid = TaskId.parse(answer.task_id) diff --git a/timApp/auth/login.py b/timApp/auth/login.py index 2a8d3269c5..91fb75748f 100644 --- a/timApp/auth/login.py +++ b/timApp/auth/login.py @@ -10,6 +10,7 @@ from flask import session from flask import url_for from flask.sessions import SecureCookieSession +from sqlalchemy import select, delete from timApp.admin.user_cli import do_merge_users, do_soft_delete from timApp.auth.accesshelper import ( @@ -266,7 +267,11 @@ def do_email_signup_or_password_reset( password_hash = create_password_hash(password) new_password = True if not is_simple_email_login_enabled(): - nu = NewUser.query.filter_by(email=email).first() + nu = ( + db.session.execute(select(NewUser).filter_by(email=email).limit(1)) + .scalars() + .first() + ) if nu: nu.pass_ = password_hash new_password = False @@ -312,9 +317,10 @@ def check_temp_pw(email_or_username: str, oldpass: str) -> NewUser: name_filter = [u.name, u.email] else: name_filter = [email_or_username] - nus = NewUser.query.filter(NewUser.email.in_(name_filter)) valid_nu = None - for nu in nus: + for nu in db.session.execute( + select(NewUser).filter(NewUser.email.in_(name_filter)) + ).scalars(): if nu.check_password(oldpass): valid_nu = nu if not valid_nu: @@ -396,8 +402,10 @@ def email_signup_finish( ) db.session.flush() - NewUser.query.filter(NewUser.email.in_((user.name, user.email))).delete( - synchronize_session=False + db.session.execute( + delete(NewUser) + .where(NewUser.email.in_((user.name, user.email))) + .execution_options(synchronize_session=False) ) db.session.flush() set_user_to_session(user) @@ -566,16 +574,18 @@ def quick_login(username: str) -> Response: if user == User.get_model_answer_user(): curr_user = get_current_user_object() - if not ( - User.query.join(UserGroup, User.groups) + stmt = ( + select(User.id) + .join(UserGroup, User.groups) .filter(User.id == curr_user.id) .filter( UserGroup.name.in_( current_app.config["QUICKLOGIN_ALLOWED_MODEL_ANSWER_GROUPS"] ) ) - .with_entities(User.id) - .first() + ) + if not ( + db.session.execute(stmt.limit(1)).scalars().first() and not check_admin_access(user=user) ): raise AccessDenied("Sorry, you don't have permission to quickLogin.") diff --git a/timApp/auth/oauth2/oauth2.py b/timApp/auth/oauth2/oauth2.py index 5946073b16..24a13d7a27 100644 --- a/timApp/auth/oauth2/oauth2.py +++ b/timApp/auth/oauth2/oauth2.py @@ -1,5 +1,4 @@ import time -from typing import Optional from authlib.integrations.flask_oauth2 import AuthorizationServer, ResourceProtector from authlib.integrations.sqla_oauth2 import ( @@ -9,6 +8,7 @@ from authlib.oauth2 import OAuth2Request from authlib.oauth2.rfc6749 import grants from flask import Flask +from sqlalchemy import select, delete from timApp.auth.oauth2.models import OAuth2Client, OAuth2Token, OAuth2AuthorizationCode from timApp.timdb.sqa import db @@ -26,13 +26,19 @@ class RefreshTokenGrant(grants.RefreshTokenGrant): INCLUDE_NEW_REFRESH_TOKEN = True def authenticate_refresh_token(self, refresh_token: str) -> OAuth2Token | None: - token: OAuth2Token = OAuth2Token.query.filter_by(refresh_token=refresh_token) + token: OAuth2Token = ( + db.session.execute( + select(OAuth2Token).filter_by(refresh_token=refresh_token).limit(1) + ) + .scalars() + .first() + ) if token and not token.is_revoked() and not token.is_expired(): return token return None def authenticate_user(self, credential: OAuth2Token) -> User: - return User.query.get(credential.user_id) + return db.session.get(User, credential.user_id) def revoke_old_credential(self, credential: OAuth2Token) -> None: credential.refresh_token_revoked_at = int(time.time()) @@ -64,9 +70,15 @@ def save_authorization_code( def query_authorization_code( self, code: str, client: OAuth2Client ) -> OAuth2AuthorizationCode | None: - auth_code = OAuth2AuthorizationCode.query.filter_by( - code=code, client_id=client.client_id - ).first() + auth_code = ( + db.session.execute( + select(OAuth2AuthorizationCode).filter_by( + code=code, client_id=client.client_id + ) + ) + .scalars() + .first() + ) if auth_code and not auth_code.is_expired(): return auth_code return None @@ -78,7 +90,7 @@ def delete_authorization_code( db.session.commit() def authenticate_user(self, authorization_code: OAuth2AuthorizationCode) -> User: - return User.query.get(authorization_code.user_id) + return db.session.get(User, authorization_code.user_id) def query_client(client_id: str) -> OAuth2Client: @@ -96,11 +108,13 @@ def query_client(client_id: str) -> OAuth2Client: def delete_expired_oauth2_tokens() -> None: now_time = int(time.time()) - OAuth2Token.query.filter( - (OAuth2Token.expires_in + OAuth2Token.issued_at < now_time) - | (OAuth2Token.access_token_revoked_at < now_time) - | (OAuth2Token.refresh_token_revoked_at < now_time) - ).delete() + db.session.execute( + delete(OAuth2Token).where( + (OAuth2Token.expires_in + OAuth2Token.issued_at < now_time) + | (OAuth2Token.access_token_revoked_at < now_time) + | (OAuth2Token.refresh_token_revoked_at < now_time) + ) + ) db.session.commit() diff --git a/timApp/auth/session/routes.py b/timApp/auth/session/routes.py index 53f20a1fb2..ef7da2ed71 100644 --- a/timApp/auth/session/routes.py +++ b/timApp/auth/session/routes.py @@ -8,6 +8,7 @@ from typing import Any from flask import Response +from sqlalchemy import update, select from timApp.auth.accesshelper import verify_logged_in, verify_admin from timApp.auth.session.model import UserSession @@ -95,27 +96,27 @@ def get_all_sessions( :return: User sessions information in the specified format. """ verify_admin() - q = UserSession.query + stmt = select(UserSession) match state: case SessionStateFilterOptions.ACTIVE: - q = q.filter(UserSession.expired == False) + stmt = stmt.filter(UserSession.expired == False) case SessionStateFilterOptions.EXPIRED: - q = q.filter(UserSession.expired == True) + stmt = stmt.filter(UserSession.expired == True) case _: pass if user: - q = q.join(User).filter(User.name == user) + stmt = stmt.join(User).filter(User.name == user) match export_format: case ExportFormatOptions.JSON: - return json_response(q.all()) + return json_response(db.session.execute(stmt).scalars().all()) case ExportFormatOptions.CSV: data: list[list[Any]] = [ ["user", "session_id", "origin", "logged_in_at", "expired_at"] ] - for s in q.all(): # type: UserSession + for s in db.session.execute(stmt).scalars().all(): # type: UserSession data.append( [ s.user.name, @@ -203,9 +204,10 @@ def validate_all() -> Response: verify_admin() all_usersnames: list[tuple[str]] = ( - db.session.query(User.name) - .join(UserSession) - .distinct(UserSession.user_id) + db.session.execute( + select(User.name).join(UserSession).distinct(UserSession.user_id) + ) + .scalars() .all() ) for (user,) in all_usersnames: @@ -222,8 +224,10 @@ def invalidate_all() -> Response: """ verify_admin() - UserSession.query.filter(~UserSession.expired).update( - {"expired_at": get_current_time()} + db.session.execute( + update(UserSession) + .where(~UserSession.expired) + .values({"expired_at": get_current_time()}) ) db.session.commit() return ok_response() diff --git a/timApp/auth/session/util.py b/timApp/auth/session/util.py index 7bfac3ef76..fd9677a619 100644 --- a/timApp/auth/session/util.py +++ b/timApp/auth/session/util.py @@ -3,7 +3,7 @@ """ from flask import has_request_context, session, current_app -from sqlalchemy import func +from sqlalchemy import func, select, update from timApp.auth.session.model import UserSession from timApp.auth.sessioninfo import get_current_user_object @@ -27,10 +27,10 @@ def _max_concurrent_sessions() -> int | None: def _get_active_session_count(user: User) -> int: - return ( - db.session.query(func.count(UserSession.session_id)) - .filter((UserSession.user == user) & ~UserSession.expired) - .scalar() + return db.session.scalar( + select(func.count(UserSession.session_id)).filter( + (UserSession.user == user) & ~UserSession.expired + ) ) @@ -69,7 +69,13 @@ def expire_user_session(user: User, session_id: str | None) -> None: if not _save_sessions() or not session_id: return - sess = UserSession.query.filter_by(user=user, session_id=session_id).first() + sess = ( + db.session.execute( + select(UserSession).filter_by(user=user, session_id=session_id) + ) + .scalars() + .first() + ) if sess: log_info( f"SESSION: {user.name} logged out (expired={sess.expired}, active={_get_active_session_count(user) - 1})" @@ -153,12 +159,16 @@ def has_valid_session(user: User | None = None) -> bool: return False current_session = ( - db.session.query(UserSession.session_id) - .filter( - (UserSession.user == user) - & (UserSession.session_id == session_id) - & ~UserSession.expired + db.session.execute( + select(UserSession.session_id) + .filter( + (UserSession.user == user) + & (UserSession.session_id == session_id) + & ~UserSession.expired + ) + .limit(1) ) + .scalars() .first() ) @@ -172,29 +182,33 @@ def verify_session_for(username: str, session_id: str | None = None) -> None: :param username: Username of the user to verify the session for. :param session_id: If specified, verify the specific session ID. If None, verify the latest added session. """ - user_subquery = db.session.query(User.id).filter(User.name == username).subquery() - q_base = UserSession.query.filter(UserSession.user_id.in_(user_subquery)) + user_subquery = select(User.id).filter(User.name == username).subquery() + stmt_base = ( + update(UserSession) + .where(UserSession.user_id.in_(user_subquery)) + .execution_options(synchronize_session=False) + ) if session_id: - q_expire = q_base.filter(UserSession.session_id != session_id) - q_verify = q_base.filter(UserSession.session_id == session_id) + stmt_expire = stmt_base.where(UserSession.session_id != session_id) + stmt_verify = stmt_base.where(UserSession.session_id == session_id) else: # Get the latest session subquery = ( - db.session.query(UserSession.session_id) + select(UserSession.session_id) .filter(UserSession.user_id.in_(user_subquery)) .order_by(UserSession.logged_in_at.desc()) .limit(1) .subquery() ) - q_expire = q_base.filter(UserSession.session_id.notin_(subquery)) - q_verify = q_base.filter(UserSession.session_id.in_(subquery)) + stmt_expire = stmt_base.where(UserSession.session_id.notin_(subquery)) + stmt_verify = stmt_base.where(UserSession.session_id.in_(subquery)) # Only expire active sessions - q_expire = q_expire.filter(UserSession.expired == False) + stmt_expire = stmt_expire.where(UserSession.expired == False) - q_expire.update({"expired_at": get_current_time()}, synchronize_session=False) - q_verify.update({"expired_at": None}, synchronize_session=False) + db.session.execute(stmt_expire.values({"expired_at": get_current_time()})) + db.session.execute(stmt_verify.values({"expired_at": None})) def invalidate_sessions_for(username: str, session_id: str | None = None) -> None: @@ -204,13 +218,17 @@ def invalidate_sessions_for(username: str, session_id: str | None = None) -> Non :param username: Username of the user to invalidate the session for. :param session_id: If specified, invalidate the specific session ID. If None, invalidate all sessions. """ - user_subquery = db.session.query(User.id).filter(User.name == username).subquery() - q_invalidate = UserSession.query.filter(UserSession.user_id.in_(user_subquery)) - + user_subquery = select(User.id).filter(User.name == username).subquery() + stmt_invalidate = ( + update(UserSession) + .filter(UserSession.user_id.in_(user_subquery)) + .values({"expired_at": get_current_time()}) + .execution_options(synchronize_session=False) + ) if session_id: - q_invalidate = q_invalidate.filter(UserSession.session_id == session_id) + stmt_invalidate = stmt_invalidate.filter(UserSession.session_id == session_id) - q_invalidate.update({"expired_at": get_current_time()}, synchronize_session=False) + db.session.execute(stmt_invalidate) def distribute_session_verification( diff --git a/timApp/auth/sessioninfo.py b/timApp/auth/sessioninfo.py index 481e133549..4ad4d840c6 100644 --- a/timApp/auth/sessioninfo.py +++ b/timApp/auth/sessioninfo.py @@ -1,10 +1,14 @@ from textwrap import dedent from flask import session, g, request, has_request_context +from sqlalchemy import select from sqlalchemy.orm import joinedload from timApp.document.usercontext import UserContext -from timApp.user.user import User, user_query_with_joined_groups +from timApp.timdb.sqa import db +from timApp.user.user import ( + User, +) from timApp.user.usergroup import UserGroup @@ -20,10 +24,10 @@ def clear_session() -> None: def get_current_user_object() -> User: if not hasattr(g, "user"): curr_id = get_current_user_id() - u = ( - user_query_with_joined_groups() - .options(joinedload(User.lectures)) - .get(curr_id) + u: User | None = db.session.get( + User, + curr_id, + options=[joinedload(User.lectures), joinedload(User.groups)], ) if u is None: if curr_id != 0: @@ -78,7 +82,11 @@ def get_other_session_users_objs() -> list[User]: def get_users_objs(lis) -> list[User]: - return User.query.filter(User.id.in_([u["id"] for u in lis])).all() + return ( + db.session.execute(select(User).filter(User.id.in_([u["id"] for u in lis]))) + .scalars() + .all() + ) def get_session_users_ids() -> list[int]: diff --git a/timApp/backup/backup_routes.py b/timApp/backup/backup_routes.py index 8ffb08a383..5502424404 100644 --- a/timApp/backup/backup_routes.py +++ b/timApp/backup/backup_routes.py @@ -1,4 +1,5 @@ from flask import Response +from sqlalchemy import select from timApp.answer.backup import save_answer_backup from timApp.answer.exportedanswer import ExportedAnswer @@ -49,15 +50,15 @@ def receive_user_memberships( user.add_to_group(ug, None) if removed_memberships: - removed_memberships_objs: list[UserGroupMember] = ( - UserGroupMember.query.join(UserGroup, UserGroupMember.group) + removed_memberships_objs: list[UserGroupMember] = db.session.execute( + select(UserGroupMember).join(UserGroup, UserGroupMember.group) .join(User, UserGroupMember.user) .filter( (User.name == user.name) & UserGroup.name.in_(removed_memberships) & membership_current ) - ).all() + ).scalars() for ugm in removed_memberships_objs: ugm.set_expired() diff --git a/timApp/bookmark/course.py b/timApp/bookmark/course.py index 0d9bbeeec1..b291b7b50f 100644 --- a/timApp/bookmark/course.py +++ b/timApp/bookmark/course.py @@ -1,3 +1,5 @@ +from sqlalchemy import select + from timApp.auth.sessioninfo import get_current_user_object from timApp.bookmark.bookmarks import Bookmarks, HIDDEN_COURSES_GROUP, MY_COURSES_GROUP from timApp.document.course.validate import is_course @@ -5,6 +7,7 @@ from timApp.document.docinfo import DocInfo from timApp.item.block import Block from timApp.item.tag import Tag, GROUP_TAG_PREFIX +from timApp.timdb.sqa import db from timApp.user.usergroup import UserGroup from timApp.util.utils import get_current_time @@ -14,16 +17,15 @@ def update_user_course_bookmarks() -> None: now = get_current_time() for gr in u.groups: # type: UserGroup if gr.is_sisu_student_group or gr.is_self_join_course: - docs = ( - DocEntry.query.join(Block) + docs = db.session.execute( + select(DocEntry).join(Block) .join(Tag) .filter( (Tag.name == GROUP_TAG_PREFIX + gr.name) & ((Tag.expires == None) | (Tag.expires > now)) ) - .with_entities(DocEntry) - .all() - ) + + ).scalars().all() if not docs: continue if len(docs) > 1: diff --git a/timApp/celery_sqlalchemy_scheduler/models.py b/timApp/celery_sqlalchemy_scheduler/models.py index 585e132fb9..ebf4cc7a99 100644 --- a/timApp/celery_sqlalchemy_scheduler/models.py +++ b/timApp/celery_sqlalchemy_scheduler/models.py @@ -1,17 +1,16 @@ import datetime as dt -import pytz +import pytz import sqlalchemy as sa +from celery import schedules +from celery.utils.log import get_logger from sqlalchemy import func from sqlalchemy.event import listen from sqlalchemy.orm import relationship, foreign, remote from sqlalchemy.sql import select, insert, update -from celery import schedules -from celery.utils.log import get_logger - -from .tzcrontab import TzAwareCrontab from .session import ModelBase +from .tzcrontab import TzAwareCrontab from ..item.block import Block from ..plugin.taskid import TaskId from ..util.utils import cached_property @@ -67,8 +66,10 @@ def schedule(self): def from_schedule(cls, session, schedule, period=SECONDS): every = max(schedule.run_every.total_seconds(), 0) model = ( - session.query(IntervalSchedule) - .filter_by(every=every, period=period) + session.execute( + select(IntervalSchedule).filter_by(every=every, period=period).limit(1) + ) + .scalars() .first() ) if not model: @@ -126,7 +127,11 @@ def from_schedule(cls, session, schedule): } if schedule.tz: spec.update({"timezone": schedule.tz.zone}) - model = session.query(CrontabSchedule).filter_by(**spec).first() + model = ( + session.execute(select(CrontabSchedule).filter_by(**spec).limit(1)) + .scalars() + .first() + ) if not model: model = cls(**spec) session.add(model) @@ -157,7 +162,9 @@ def from_schedule(cls, session, schedule): "latitude": schedule.lat, "longitude": schedule.lon, } - model = session.query(SolarSchedule).filter_by(**spec).first() + model = ( + session.execute(select(SolarSchedule).filter_by(**spec)).scalars().first() + ) if not model: model = cls(**spec) session.add(model) @@ -211,13 +218,16 @@ def update_changed(cls, mapper, connection, target): @classmethod def last_change(cls, session): - periodic_tasks = session.query(PeriodicTaskChanged).get(1) + periodic_tasks = ( + session.execute(select(PeriodicTaskChanged).filter_by(id=1)) + .scalars() + .first() + ) if periodic_tasks: return periodic_tasks.last_update class PeriodicTask(ModelBase, ModelMixin): - __tablename__ = "celery_periodic_task" __table_args__ = {"sqlite_autoincrement": True} diff --git a/timApp/celery_sqlalchemy_scheduler/schedulers.py b/timApp/celery_sqlalchemy_scheduler/schedulers.py index 7769fe0eea..e2a0ce38ca 100644 --- a/timApp/celery_sqlalchemy_scheduler/schedulers.py +++ b/timApp/celery_sqlalchemy_scheduler/schedulers.py @@ -9,6 +9,7 @@ from celery.utils.time import maybe_make_aware from kombu.utils.encoding import safe_str, safe_repr from kombu.utils.json import dumps, loads +from sqlalchemy import select from .models import ( PeriodicTask, @@ -191,7 +192,7 @@ def save(self, fields=tuple()): with session_cleanup(session): # Object may not be synchronized, so only # change the fields we care about. - obj = session.query(PeriodicTask).get(self.model.id) + obj = session.get(PeriodicTask, self.model.id) for field in self.save_fields: setattr(obj, field, getattr(self.model, field)) @@ -224,7 +225,11 @@ def from_entry(cls, name, Session, app=None, **entry): """ session = Session() with session_cleanup(session): - periodic_task = session.query(PeriodicTask).filter_by(name=name).first() + periodic_task = ( + session.execute(select(PeriodicTask).filter_by(name=name).limit(1)) + .scalars() + .first() + ) if not periodic_task: periodic_task = PeriodicTask(name=name) temp = cls._unpack_fields(session, **entry) @@ -349,7 +354,11 @@ def all_as_schedule(self): with session_cleanup(session): logger.debug("DatabaseScheduler: Fetching database schedule") # get all enabled PeriodicTask - models = session.query(self.Model).filter_by(enabled=True).all() + models = ( + session.execute(select(self.Model).filter_by(enabled=True)) + .scalars() + .all() + ) s = {} for model in models: try: @@ -363,7 +372,7 @@ def all_as_schedule(self): def schedule_changed(self): session = self.Session() with session_cleanup(session): - changes = session.query(self.Changes).get(1) + changes = session.get(self.Changes, 1) if not changes: changes = self.Changes(id=1) session.add(changes) diff --git a/timApp/document/changelog.py b/timApp/document/changelog.py index 202f346e32..695f0ba03e 100644 --- a/timApp/document/changelog.py +++ b/timApp/document/changelog.py @@ -1,6 +1,8 @@ from collections import defaultdict from typing import List, Tuple, Optional +from sqlalchemy import select + import timApp from timApp.document.changelogentry import ChangelogEntry from timApp.document.docparagraph import DocParagraph @@ -58,9 +60,13 @@ def get_authorinfo(self, pars: list[DocParagraph]) -> dict[str, AuthorInfo]: User = timApp.user.user.User UserGroup = timApp.user.usergroup.UserGroup result = ( - db.session.query(UserGroup, User) - .filter(UserGroup.id.in_(usergroup_ids)) - .outerjoin(User, User.name == UserGroup.name) + db.session.execute( + select(UserGroup, User) + .select_from(UserGroup) + .filter(UserGroup.id.in_(usergroup_ids)) + .outerjoin(User, User.name == UserGroup.name) + ) + .scalars() .all() ) # type: List[Tuple[UserGroup, Optional[User]]] for ug, u in result: diff --git a/timApp/document/changelogentry.py b/timApp/document/changelogentry.py index bbee340429..b5565e951c 100644 --- a/timApp/document/changelogentry.py +++ b/timApp/document/changelogentry.py @@ -5,6 +5,7 @@ import dateutil.parser from timApp.document.version import Version +from timApp.timdb.sqa import db from timApp.user.usergroup import UserGroup @@ -86,7 +87,7 @@ def to_json(self): "ver": self.version, "time": self.time, "op": self.op.op.value, - "group": UserGroup.query.get(self.group_id).name, + "group": db.session.get(UserGroup, self.group_id).name, "par_id": self.par_id, "op_params": self.op.to_json(), } diff --git a/timApp/document/docentry.py b/timApp/document/docentry.py index bed1c1f78d..91d8bc6ba0 100644 --- a/timApp/document/docentry.py +++ b/timApp/document/docentry.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any +from sqlalchemy import select from sqlalchemy.orm import foreign from timApp.document.docinfo import DocInfo @@ -93,11 +94,11 @@ def translations(self) -> list[Translation]: @staticmethod def get_all() -> list[DocEntry]: - return DocEntry.query.all() + return db.session.execute(select(DocEntry)).scalars().all() @staticmethod def find_all_by_id(doc_id: int) -> list[DocEntry]: - return DocEntry.query.filter_by(id=doc_id).all() + return db.session.execute(select(DocEntry).filter_by(id=doc_id)).scalars().all() @staticmethod def find_by_id(doc_id: int, docentry_load_opts: Any = None) -> DocInfo | None: @@ -105,13 +106,13 @@ def find_by_id(doc_id: int, docentry_load_opts: Any = None) -> DocInfo | None: TODO: This method doesn't really belong in DocEntry class. """ - q = DocEntry.query.filter_by(id=doc_id) + stmt = select(DocEntry).filter_by(id=doc_id) if docentry_load_opts: - q = q.options(*docentry_load_opts) - d = q.first() + stmt = stmt.options(*docentry_load_opts) + d = db.session.execute(stmt.limit(1)).scalars().first() if d: return d - return Translation.query.get(doc_id) + return db.session.get(Translation, doc_id) @staticmethod def find_by_path( @@ -126,7 +127,7 @@ def find_by_path( """ if docentry_load_opts is None: docentry_load_opts = [] - d = DocEntry.query.options(*docentry_load_opts).get(path) + d = db.session.get(DocEntry, path, options=docentry_load_opts) if d: return d # try translation @@ -140,10 +141,16 @@ def find_by_path( if entry is not None: # Match lang id using LIKE to allow for partial matches. # This is a simple way to allow mapping /en to newer /en-US or /en-GB. - tr = Translation.query.filter( - (Translation.src_docid == entry.id) - & (Translation.lang_id.like(f"{lang}%")) - ).first() + tr = ( + db.session.execute( + select(Translation).filter( + (Translation.src_docid == entry.id) + & (Translation.lang_id.like(f"{lang}%")) + ) + ) + .scalars() + .first() + ) if tr is not None: tr.docentry = entry return tr @@ -249,21 +256,21 @@ def get_documents( """ - q = DocEntry.query + stmt = select(DocEntry) if not include_nonpublic: - q = q.filter_by(public=True) + stmt = stmt.filter_by(public=True) if filter_folder is not None: filter_folder = filter_folder.strip("/") + "/" if filter_folder == "/": filter_folder = "" - q = q.filter(DocEntry.name.like(filter_folder + "%")) + stmt = stmt.filter(DocEntry.name.like(filter_folder + "%")) if not search_recursively: - q = q.filter(DocEntry.name.notlike(filter_folder + "%/%")) + stmt = stmt.filter(DocEntry.name.notlike(filter_folder + "%/%")) if custom_filter is not None: - q = q.filter(custom_filter) + stmt = stmt.filter(custom_filter) if query_options is not None: - q = q.options(query_options) - result = q.all() + stmt = stmt.options(query_options) + result = db.session.execute(stmt).scalars().all() if not filter_user: return result return [r for r in result if filter_user.has_view_access(r)] diff --git a/timApp/document/docinfo.py b/timApp/document/docinfo.py index b7155e8424..8b1296b73a 100644 --- a/timApp/document/docinfo.py +++ b/timApp/document/docinfo.py @@ -3,6 +3,7 @@ from itertools import accumulate from typing import Iterable, Generator, TYPE_CHECKING +from sqlalchemy import select from sqlalchemy.orm import joinedload from timApp.document.docparagraph import DocParagraph @@ -174,16 +175,16 @@ def absolute_path(variable: str) -> bool: from timApp.document.translation.translation import Translation def get_docs(doc_paths: list[str]) -> list[tuple[DocEntry, Translation | None]]: - return ( - db.session.query(DocEntry, Translation) + return db.session.execute( + select(DocEntry, Translation) + .select_from(DocEntry) .filter(DocEntry.name.in_(doc_paths)) .outerjoin( Translation, (Translation.src_docid == DocEntry.id) & (Translation.lang_id == self.lang_id), ) - .all() - ) + ).scalars().all() result = get_docs(paths) result.sort(key=lambda x: path_index_map[x[0].path]) @@ -243,11 +244,13 @@ def get_notifications(self, condition) -> list[Notification]: items.add(self) from timApp.user.user import User - q = Notification.query.options( - joinedload(Notification.user).joinedload(User.groups) - ).filter(Notification.block_id.in_([f.id for f in items])) - q = q.filter(condition) - return q.all() + stmt = ( + select(Notification) + .options(joinedload(Notification.user).joinedload(User.groups)) + .filter(Notification.block_id.in_([f.id for f in items])) + ) + stmt = stmt.filter(condition) + return db.session.execute(stmt).scalars().all() def has_translation(self, lang_id): for t in self.translations: diff --git a/timApp/document/documents.py b/timApp/document/documents.py index deeb08ed1b..7fe372e78e 100644 --- a/timApp/document/documents.py +++ b/timApp/document/documents.py @@ -1,6 +1,6 @@ """Defines the Documents class.""" -from typing import Optional +from sqlalchemy import delete from timApp.auth.auth_models import BlockAccess from timApp.document.docentry import DocEntry @@ -12,6 +12,7 @@ from timApp.item.block import Block, BlockType from timApp.note.usernote import UserNote from timApp.readmark.readparagraph import ReadParagraph +from timApp.timdb.sqa import db from timApp.user.usergroup import UserGroup @@ -138,14 +139,18 @@ def delete_document(document_id: int): """ - DocEntry.query.filter_by(id=document_id).delete() - BlockAccess.query.filter_by(block_id=document_id).delete() - Block.query.filter_by(type_id=BlockType.Document.value, id=document_id).delete() - ReadParagraph.query.filter_by(doc_id=document_id).delete() - UserNote.query.filter_by(doc_id=document_id).delete() - Translation.query.filter( - (Translation.doc_id == document_id) | (Translation.src_docid == document_id) - ).delete() + for stmt in ( + delete(DocEntry).where(DocEntry.id == document_id), + delete(BlockAccess).where(BlockAccess.block_id == document_id), + delete(Block).where((Block.type_id == BlockType.Document.value) & (Block.id == document_id)), + delete(ReadParagraph).where(ReadParagraph.doc_id == document_id), + delete(UserNote).where(UserNote.doc_id == document_id), + delete(Translation).where( + (Translation.doc_id == document_id) | (Translation.src_docid == document_id) + ) + ): + db.session.execute(stmt) + Document.remove(document_id) diff --git a/timApp/document/editing/routes.py b/timApp/document/editing/routes.py index a98518ecc9..db32604208 100644 --- a/timApp/document/editing/routes.py +++ b/timApp/document/editing/routes.py @@ -5,6 +5,7 @@ from flask import Blueprint, render_template from flask import current_app from flask import request +from sqlalchemy import select from timApp.admin.associate_old_uploads import upload_regexes from timApp.answer.answer import Answer @@ -55,7 +56,6 @@ from timApp.plugin.qst.qst import question_convert_js_to_yaml from timApp.plugin.save_plugin import save_plugin from timApp.readmark.readings import mark_read - # from timApp.timdb.dbaccess import get_timdb from timApp.timdb.exceptions import TimDbException from timApp.timdb.sqa import db @@ -763,7 +763,7 @@ def check_duplicates(pars, doc): duplicate.append(task_id) duplicate.append(par.get_id()) task_id_to_check = str(doc.doc_id) + "." + task_id - if Answer.query.filter_by(task_id=task_id_to_check).first(): + if db.session.execute(select(Answer).filter_by(task_id=task_id_to_check)).scalars().first(): duplicate.append("hasAnswers") duplicates.append(duplicate) break diff --git a/timApp/document/translation/deepl.py b/timApp/document/translation/deepl.py index 3714362252..101fee63f1 100644 --- a/timApp/document/translation/deepl.py +++ b/timApp/document/translation/deepl.py @@ -18,6 +18,7 @@ import langcodes from requests import post, Response from requests.exceptions import JSONDecodeError +from sqlalchemy import select from timApp.document.translation.language import Language from timApp.document.translation.translationparser import TranslateApproval, NoTranslate @@ -32,11 +33,10 @@ from timApp.timdb.sqa import db from timApp.user.usergroup import UserGroup from timApp.util import logger -from timApp.util.flask.requesthelper import NotExist, RouteException from timApp.util.flask.cache import cache +from timApp.util.flask.requesthelper import NotExist, RouteException from tim_common.vendor.requests_futures import FuturesSession, Future - LANGUAGES_CACHE_TIMEOUT = 3600 * 24 # seconds @@ -82,10 +82,10 @@ def register(self, user_group: UserGroup) -> None: :raises RouteException: If more than one key is found from user. """ # One user group should match one service per one key. - api_key = TranslationServiceKey.query.filter( + api_key = db.session.execute(select(TranslationServiceKey).filter( TranslationServiceKey.service_id == self.id, TranslationServiceKey.group_id == user_group.id, - ).all() + )).scalars().all() if len(api_key) == 0: raise NotExist( "Please add a DeepL API key that corresponds the chosen plan into your account" diff --git a/timApp/document/translation/language.py b/timApp/document/translation/language.py index 36dc406736..d6e07be0f2 100644 --- a/timApp/document/translation/language.py +++ b/timApp/document/translation/language.py @@ -18,6 +18,7 @@ from typing import Optional import langcodes +from sqlalchemy import select from timApp.timdb.sqa import db @@ -92,7 +93,7 @@ def query_by_code(cls, code: str) -> Optional["Language"]: """ # TODO Instead of the code -parameter being str-type, could # langcodes.Language type be more convenient to caller? - return cls.query.get(code) + return db.session.get(cls, code) @classmethod def query_all(cls) -> list["Language"]: @@ -101,7 +102,7 @@ def query_all(cls) -> list["Language"]: :return: All the languages found from database. """ - return cls.query.all() + return db.session.execute(select(cls)).scalars().all() def __str__(self) -> str: """ diff --git a/timApp/document/translation/routes.py b/timApp/document/translation/routes.py index b4dba65129..6413ea0e6c 100644 --- a/timApp/document/translation/routes.py +++ b/timApp/document/translation/routes.py @@ -21,6 +21,7 @@ import langcodes import requests from flask import request, Blueprint +from sqlalchemy import select, delete from sqlalchemy.exc import IntegrityError from timApp.auth.accesshelper import ( @@ -33,15 +34,11 @@ verify_edit_access, ) from timApp.auth.sessioninfo import get_current_user_object -from timApp.item.copy_rights import copy_rights -from timApp.document.document import Document from timApp.document.docentry import create_document_and_block, DocEntry +from timApp.document.document import Document from timApp.document.documents import add_reference_pars +from timApp.document.translation.language import Language from timApp.document.translation.translation import Translation -from timApp.timdb.exceptions import ItemAlreadyExistsException -from timApp.timdb.sqa import db -from timApp.util.flask.requesthelper import verify_json_params, NotExist, RouteException -from timApp.util.flask.responsehelper import json_response, ok_response, Response from timApp.document.translation.translator import ( TranslationService, RegisteredTranslationService, @@ -49,8 +46,12 @@ TranslationTarget, TranslateProcessor, ) -from timApp.document.translation.language import Language +from timApp.item.copy_rights import copy_rights from timApp.item.routes import get_document_relevance, set_relevance +from timApp.timdb.exceptions import ItemAlreadyExistsException +from timApp.timdb.sqa import db +from timApp.util.flask.requesthelper import verify_json_params, NotExist, RouteException +from timApp.util.flask.responsehelper import json_response, ok_response, Response def is_valid_language_id(lang_id: str) -> bool: @@ -152,11 +153,10 @@ def get_languages(source_languages: bool) -> Response: else: # Get the translation service by the provided service name # TODO Maybe change to use an id instead? - tr = ( - TranslationService.query.with_polymorphic("*") + tr = db.session.execute( + select(TranslationService).with_polymorphic("*") .filter(TranslationService.service_name == translator) - .one() - ) + ).scalars().one() if isinstance(tr, RegisteredTranslationService): tr.register(get_current_user_object().get_personal_group()) @@ -398,7 +398,7 @@ def get_all_languages() -> Response: :return: JSON response containing all the available languages. """ - langs = sorted(Language.query.all()) + langs = sorted(db.session.execute(select(Language)).scalars().all()) return json_response(langs) @@ -421,9 +421,7 @@ def get_translators() -> Response: :return: JSON response containing the translators. """ - translationservice_names = TranslationService.query.with_entities( - TranslationService.service_name - ).all() + translationservice_names = db.session.execute(select(TranslationService.service_name)).scalars().all() # The SQLAlchemy query returns a list of tuples even when values of a # single column were requested, so they must be unpacked. # TODO Add "Manual" to the TranslationService-table instead of hardcoding @@ -444,16 +442,16 @@ def add_api_key() -> Response: translator = req_data.get("translator", "") key = req_data.get("apikey", "") - tr = TranslationService.query.filter( + tr = db.session.execute(select(TranslationService).filter( translator == TranslationService.service_name - ).first() + )).scalars().first() verify_logged_in() user = get_current_user_object() - duplicate = TranslationServiceKey.query.filter( - tr.id == TranslationServiceKey.service_id, - user.get_personal_group().id == TranslationServiceKey.group_id, - ).first() + duplicate = db.session.execute(select(TranslationServiceKey).filter( + (tr.id == TranslationServiceKey.service_id) & + (user.get_personal_group().id == TranslationServiceKey.group_id) + )).scalars().first() if duplicate: raise RouteException("There is already a key for this translator for this user") @@ -483,11 +481,11 @@ def remove_api_key() -> Response: translator = req_data.get("translator", "") key = req_data.get("apikey", "") - TranslationServiceKey.query.filter( - key == TranslationServiceKey.api_key, - TranslationServiceKey.group_id == user.get_personal_group().id, - translator == TranslationService.service_name, - ).delete(synchronize_session=False) + db.session.execution(delete(TranslationServiceKey).filter( + (key == TranslationServiceKey.api_key) & + (TranslationServiceKey.group_id == user.get_personal_group().id) & + (translator == TranslationService.service_name) + ).execution_options(synchronize_session=False)) db.session.commit() @@ -511,9 +509,9 @@ def get_quota(): # Get the translation service by the provided service name. # TODO Maybe change to use id instead? - tr = TranslationService.query.filter( - translator == TranslationService.service_name, - ).first() + tr = db.session.execute(select(TranslationService).filter( + translator == TranslationService.service_name + )).scalars().first() tr.register(get_current_user_object().get_personal_group()) return json_response(tr.usage()) @@ -534,9 +532,9 @@ def get_valid_status() -> Response: key = req_data.get("apikey", "") # Get the translation service by the provided service name. - tr = TranslationService.query.filter( + tr = db.session.execute(select(TranslationService).filter( translator == TranslationService.service_name, - ).first() + )).scalars().first() # Each new translator engine should add their preferred method for # validating api keys here. @@ -566,9 +564,9 @@ def get_keys() -> Response: verify_logged_in() user = get_current_user_object() - keys = TranslationServiceKey.query.filter( + keys = db.session.execute(select(TranslationServiceKey).filter( TranslationServiceKey.group_id == user.get_personal_group().id - ).all() + )).scalars().all() return json_response(keys) @@ -584,9 +582,9 @@ def get_my_translators() -> Response: verify_logged_in() user = get_current_user_object() - keys = TranslationServiceKey.query.filter( + keys = db.session.execute(select(TranslationServiceKey).filter( TranslationServiceKey.group_id == user.get_personal_group().id - ).all() + )).scalars().all() result = [] for x in keys: diff --git a/timApp/document/translation/translator.py b/timApp/document/translation/translator.py index a7f6c46f1e..6075d5d8f8 100644 --- a/timApp/document/translation/translator.py +++ b/timApp/document/translation/translator.py @@ -21,9 +21,9 @@ from dataclasses import dataclass import pypandoc +from sqlalchemy import select +from sqlalchemy.orm import with_polymorphic -from timApp.timdb.sqa import db -from timApp.user.usergroup import UserGroup from timApp.document.docparagraph import DocParagraph from timApp.document.translation.language import Language from timApp.document.translation.translationparser import ( @@ -33,10 +33,11 @@ Table, Translate, ) +from timApp.timdb.sqa import db +from timApp.user.usergroup import UserGroup from timApp.util import logger from timApp.util.flask.requesthelper import RouteException - TranslateBlock = list[TranslateApproval] """Typedef to represent logically connected parts of non- and translatable text. """ @@ -208,9 +209,9 @@ def get_by_user_group( :return: The first matching TranslationServiceKey instance, if one is found. """ - return TranslationServiceKey.query.get( + return db.session.execute(select(TranslationServiceKey).filter( TranslationServiceKey.group_id == user_group - ) + )).first() def to_json(self) -> dict: """ @@ -278,11 +279,10 @@ def __init__( the user sets to their account). """ - translator = ( - TranslationService.query.with_polymorphic("*") + translator = db.session.execute( + select(with_polymorphic(TranslationService, "*")) .filter(TranslationService.service_name == translator_code) - .one() - ) + ).scalars().one() if user_group is not None and isinstance( translator, RegisteredTranslationService diff --git a/timApp/folder/folder.py b/timApp/folder/folder.py index 5d974dce6a..2b44a8ac30 100644 --- a/timApp/folder/folder.py +++ b/timApp/folder/folder.py @@ -2,7 +2,7 @@ from typing import Iterable, Any, TYPE_CHECKING -from sqlalchemy import true, and_ +from sqlalchemy import true, and_, select, delete from timApp.auth.auth_models import BlockAccess from timApp.document.docentry import DocEntry, get_documents @@ -46,11 +46,11 @@ def get_root() -> Folder: @staticmethod def get_by_id(fid) -> Folder | None: - return Folder.query.get(fid) if fid != ROOT_FOLDER_ID else Folder.get_root() + return db.session.get(Folder, fid) if fid != ROOT_FOLDER_ID else Folder.get_root() @staticmethod def find_by_location(location, name) -> Folder | None: - return Folder.query.filter_by(name=name, location=location).first() + return db.session.execute(select(Folder).filter_by(name=name, location=location)).scalars().first() @staticmethod def find_by_path(path, fallback_to_id=False) -> Folder | None: @@ -111,10 +111,10 @@ def get_all_in_path( if root_path else true() ) - q = Folder.query.filter(f_filter) + stmt = select(Folder).filter(f_filter) if filter_ids: - q = q.filter(Folder.id.in_(filter_ids)) - return q.all() + stmt = stmt.filter(Folder.id.in_(filter_ids)) + return db.session.execute(stmt).scalars().all() def is_root(self) -> bool: return self.id == -1 @@ -122,8 +122,8 @@ def is_root(self) -> bool: def delete(self): assert self.is_empty db.session.delete(self) - BlockAccess.query.filter_by(block_id=self.id).delete() - Block.query.filter_by(type_id=BlockType.Folder.value, id=self.id).delete() + db.session.execute(delete(BlockAccess).where(BlockAccess.block_id==self.id)) + db.session.execute(delete(Block).where((Block.type_id==BlockType.Folder.value) & (Block.id==self.id))) def rename(self, new_name: str): assert "/" not in new_name @@ -144,25 +144,25 @@ def rename_path(self, new_path: str) -> None: def rename_content(self, old_path: str, new_path: str): """Renames contents of the folder.""" - docs_in_folder: list[DocEntry] = DocEntry.query.filter( + docs_in_folder: list[DocEntry] = db.session.execute(select(DocEntry).filter( DocEntry.name.like(old_path + "/%") - ).all() + )).scalars().all() for d in docs_in_folder: d.name = d.name.replace(old_path, new_path, 1) - folders_in_folder = Folder.query.filter( + folders_in_folder = db.session.execute(select(Folder).filter( (Folder.location == old_path) | (Folder.location.like(old_path + "/%")) - ).all() + )).scalars().all() for f in folders_in_folder: f.location = f.location.replace(old_path, new_path, 1) @property def is_empty(self): - q = Folder.query.filter_by(location=self.path) - if db.session.query(q.exists()).scalar(): + stmt = select(Folder.id).filter_by(location=self.path) + if db.session.execute(stmt.limit()).first(): return False - q = DocEntry.query.filter(DocEntry.name.like(self.path + "/%")) - return not db.session.query(q.exists()).scalar() + stmt = select(DocEntry.id).filter(DocEntry.name.like(self.path + "/%")) + return not db.session.execute(stmt.limit(1)).first() @property def parent(self) -> Folder | None: @@ -201,10 +201,10 @@ def get_full_path(self) -> str: def get_document( self, relative_path: str, create_if_not_exist=False, creator_group=None - ) -> None | (DocEntry): - doc = DocEntry.query.filter_by( + ) -> None | DocEntry: + doc = db.session.execute(select(DocEntry).filter_by( name=join_location(self.get_full_path(), relative_path) - ).first() + )).scalars().first() if doc is not None: return doc if create_if_not_exist: @@ -283,7 +283,7 @@ def create( return Folder.get_root() rel_path, rel_name = split_location(path) - folder = Folder.query.filter_by(name=rel_name, location=rel_path).first() + folder = db.session.execute(select(Folder).filter_by(name=rel_name, location=rel_path)).scalars().first() if folder is not None: return folder diff --git a/timApp/gamification/gamificationdata.py b/timApp/gamification/gamificationdata.py index f646dc6634..afbbf48f1b 100644 --- a/timApp/gamification/gamificationdata.py +++ b/timApp/gamification/gamificationdata.py @@ -2,12 +2,15 @@ from collections import defaultdict from operator import itemgetter, attrgetter +from sqlalchemy import select + from timApp.answer.answers import get_users_for_tasks from timApp.auth.sessioninfo import get_current_user_id, user_context_with_logged_in from timApp.document.docentry import DocEntry from timApp.document.viewcontext import default_view_ctx from timApp.document.yamlblock import YamlBlock from timApp.plugin.plugin import find_task_ids +from timApp.timdb.sqa import db def gamify(initial_data: YamlBlock): @@ -84,7 +87,7 @@ def get_sorted_lists(items, item_name: str): filtered_items.append(item) # Sort both so they can be looped simultaneusly without value mismatches. docs = sorted( - DocEntry.query.filter(DocEntry.name.in_(item_path_list)).all(), + db.session.execute(select(DocEntry).filter(DocEntry.name.in_(item_path_list))).scalars().all(), key=attrgetter("path"), ) items = sorted(filtered_items, key=itemgetter("path")) diff --git a/timApp/item/distribute_rights.py b/timApp/item/distribute_rights.py index e55055cf8f..fa060e07f8 100644 --- a/timApp/item/distribute_rights.py +++ b/timApp/item/distribute_rights.py @@ -11,6 +11,7 @@ from flask import Response, flash, request from isodate import Duration from marshmallow import Schema +from sqlalchemy import select from werkzeug.utils import secure_filename from timApp.auth.accesshelper import AccessDenied, verify_admin @@ -258,11 +259,10 @@ def get_group_emails(self, r: GroupOp) -> list[Email]: if not emails: emails = [ e - for e, in ( - UserGroup.query.join(User, UserGroup.users) + for e, in db.session.execute( + select(User.email).join(User, UserGroup.users) .filter(UserGroup.name == r.group) - .with_entities(User.email) - ) + ).scalars() ] if not emails: if not UserGroup.get_by_name(r.group): @@ -437,12 +437,10 @@ def receive_right( secret: str, ) -> Response: check_secret(secret, "DIST_RIGHTS_RECEIVE_SECRET") - uges = ( - UserGroup.query.join(User, UserGroup.name == User.name) + uges = db.session.execute( + select(UserGroup, User.email).join(User, UserGroup.name == User.name) .filter(User.email.in_(re.email for re in rights)) - .with_entities(UserGroup, User.email) - .all() - ) + ).scalars().all() group_map = {} for ug, email in uges: group_map[email] = ug @@ -543,11 +541,9 @@ def get_current_rights_route( except FileNotFoundError: raise RouteException(f"Unknown target: {target}") groups_list = groups.split(",") - emails = ( - User.query.join(UserGroup, User.groups) + emails = db.session.execute( + select(User.email).join(UserGroup, User.groups) .filter(UserGroup.name.in_(groups_list)) - .with_entities(User.email) .order_by(User.email) - .all() - ) + ).scalars().all() return json_response([{"email": e, "right": rights.get_right(e)} for e, in emails]) diff --git a/timApp/item/item.py b/timApp/item/item.py index 3a014add0a..509ef8eec5 100644 --- a/timApp/item/item.py +++ b/timApp/item/item.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from flask import current_app -from sqlalchemy import tuple_, func +from sqlalchemy import tuple_, func, select from sqlalchemy.orm import defaultload from timApp.auth.auth_models import BlockAccess @@ -12,7 +12,7 @@ from timApp.item.block import Block, BlockType from timApp.item.blockrelevance import BlockRelevance from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import include_if_loaded +from timApp.timdb.sqa import include_if_loaded, db from timApp.util.utils import split_location, date_to_relative, cached_property if TYPE_CHECKING: @@ -31,7 +31,7 @@ def owners(self): def block(self) -> Block: # Relationships are not loaded when constructing an object with __init__. if not hasattr(self, "_block") or self._block is None: - self._block = Block.query.get(self.id) + self._block = db.session.get(Block, self.id) return self._block @property @@ -121,18 +121,19 @@ def parents_to_root(self, include_root=True, eager_load_groups=False): # TODO: Add an option whether to load relevance eagerly or not; # currently eager by default is better to speed up search cache processing # and it doesn't slow down other code much. - crumbs_q = ( - Folder.query.filter(tuple_(Folder.location, Folder.name).in_(path_tuples)) + crumbs_stmt = ( + select(Folder) + .filter(tuple_(Folder.location, Folder.name).in_(path_tuples)) .order_by(func.length(Folder.location).desc()) .options(defaultload(Folder._block).joinedload(Block.relevance)) ) if eager_load_groups: - crumbs_q = crumbs_q.options( + crumbs_stmt = crumbs_stmt.options( defaultload(Folder._block) .joinedload(Block.accesses) .joinedload(BlockAccess.usergroup) ) - crumbs = crumbs_q.all() + crumbs = db.session.execute(crumbs_stmt).scalars().all() if include_root: crumbs.append(Folder.get_root()) return crumbs @@ -149,7 +150,9 @@ def parents_to_root_eager(self): @property def parent( self, - ) -> Folder: # TODO rename this to parent_folder to distinguish better from "parents" attribute + ) -> ( + Folder + ): # TODO rename this to parent_folder to distinguish better from "parents" attribute folder = self.location from timApp.folder.folder import Folder @@ -193,7 +196,7 @@ def get_relative_path(self, path: str): @staticmethod def find_by_id(item_id): - b = Block.query.get(item_id) + b = db.session.get(Block, item_id) if b: if b.type_id == BlockType.Document.value: from timApp.document.docentry import DocEntry diff --git a/timApp/item/manage.py b/timApp/item/manage.py index f45ae28880..ca49b61efc 100644 --- a/timApp/item/manage.py +++ b/timApp/item/manage.py @@ -11,7 +11,7 @@ from flask import render_template, Response from flask import request from isodate import Duration -from sqlalchemy import inspect +from sqlalchemy import inspect, select from sqlalchemy.orm.state import InstanceState from timApp.auth.accesshelper import ( @@ -129,7 +129,7 @@ def manage(path: str) -> Response | str: js=["angular-ui-grid"], jsMods=get_grid_modules(), orgs=UserGroup.get_organizations(), - access_types=AccessTypeModel.query.all(), + access_types=db.session.execute(select(AccessTypeModel)).scalars().all(), ) @@ -210,7 +210,13 @@ def __post_init__(self): @cached_property def group_objects(self): - return UserGroup.query.filter(UserGroup.name.in_(self.groups)).all() + return ( + db.session.execute( + select(UserGroup).filter(UserGroup.name.in_(self.groups)) + ) + .scalar() + .all() + ) @property def nonexistent_groups(self): @@ -421,11 +427,17 @@ def expire_doc_velp_groups_perms(doc_id: int, ug: UserGroup) -> None: # Only expire permissions from velp groups attached to the document if is_velp_group_in_document(vg, doc): # TODO Should this apply to ALL permissions, instead of just 'view'? - acc: BlockAccess | None = BlockAccess.query.filter_by( - type=AccessType.view.value, - block_id=vg.id, - usergroup_id=ug.id, - ).first() + acc: BlockAccess | None = ( + db.session.execute( + select(BlockAccess).filter_by( + type=AccessType.view.value, + block_id=vg.id, + usergroup_id=ug.id, + ) + ) + .scalars() + .first() + ) if acc: accs.append(acc) for a in accs: @@ -446,11 +458,17 @@ def expire_doc_translation_perms(doc_id: int, ug: UserGroup) -> None: if tr.id == doc.id: continue # TODO Should this apply to ALL permissions, instead of just 'view'? - acc: BlockAccess | None = BlockAccess.query.filter_by( - type=AccessType.view.value, - block_id=tr.id, - usergroup_id=ug.id, - ).first() + acc: BlockAccess | None = ( + db.session.execute( + select(BlockAccess).filter_by( + type=AccessType.view.value, + block_id=tr.id, + usergroup_id=ug.id, + ) + ) + .scalars() + .first() + ) if acc: accs.append(acc) for a in accs: @@ -479,11 +497,17 @@ def do_confirm_permission( redir: str | None = None, confirm_translations: bool = True, ): - ba: BlockAccess | None = BlockAccess.query.filter_by( - type=m.type.value, - block_id=m.id, - usergroup_id=m.group, - ).first() + ba: BlockAccess | None = ( + db.session.execute( + select(BlockAccess).filter_by( + type=m.type.value, + block_id=m.id, + usergroup_id=m.group, + ) + ) + .scalars() + .first() + ) if not ba: return raise_or_redirect("Right not found.", redir) if not ba.require_confirm: @@ -492,7 +516,7 @@ def do_confirm_permission( redir, ) ba.do_confirm() - ug: UserGroup = UserGroup.query.get(m.group) + ug: UserGroup = db.session.get(UserGroup, m.group) log_right(f"confirmed {ba.info_str} for {ug.name} in {i.path}") if confirm_translations and i.is_original_translation: @@ -523,11 +547,15 @@ def edit_permissions(m: PermissionMassEditModel) -> Response: if nonexistent: raise RouteException(f"Non-existent groups: {nonexistent}") items: list[ItemOrBlock] = ( - Block.query.filter( - Block.id.in_(m.ids) - & Block.type_id.in_([BlockType.Document.value, BlockType.Folder.value]) + db.session.execute( + select(Block) + .filter( + Block.id.in_(m.ids) + & Block.type_id.in_([BlockType.Document.value, BlockType.Folder.value]) + ) + .order_by(Block.id) ) - .order_by(Block.id) + .scalars() .all() ) @@ -626,8 +654,7 @@ def add_perm( def remove_permission(m: PermissionRemoveModel) -> Response: i = get_item_or_abort(m.id) had_ownership = verify_permission_edit_access(i, m.type) - # ug: UserGroup = UserGroup.query.get(m.group) # query.get() is deprecated - ug: UserGroup = UserGroup.query.filter_by(id=m.group).first() + ug: UserGroup = db.session.get(UserGroup, m.group) if not ug: raise RouteException("User group not found") a = remove_perm( @@ -895,7 +922,7 @@ def get_permissions(item_id: int) -> Response: return json_response( { "grouprights": grouprights, - "accesstypes": AccessTypeModel.query.all(), + "accesstypes": db.session.execute(select(AccessTypeModel)).scalars().all(), "orgs": UserGroup.get_organizations(), }, date_conversion=True, @@ -935,7 +962,7 @@ def add_default_doc_permission(m: DefaultPermissionModel) -> Response: def remove_default_doc_permission(m: DefaultPermissionRemoveModel) -> Response: f = get_folder_or_abort(m.id) verify_permission_edit_access(f, m.type) - ug = UserGroup.query.get(m.group) + ug = db.session.get(UserGroup, m.group) if not ug: raise NotExist("Usergroup not found") remove_default_access(ug, f, m.type, BlockType.from_str(m.item_type.name)) diff --git a/timApp/item/routes.py b/timApp/item/routes.py index 275bd305b9..1375502486 100644 --- a/timApp/item/routes.py +++ b/timApp/item/routes.py @@ -16,6 +16,7 @@ from flask import session from markupsafe import Markup from marshmallow import EXCLUDE +from sqlalchemy import select from sqlalchemy.orm import joinedload, defaultload from timApp.answer.answers import add_missing_users_from_groups, get_points_by_rule @@ -378,9 +379,12 @@ def gen_cache( accesses: ValuesView[BlockAccess] = doc_info.block.accesses.values() group_ids = {a.usergroup_id for a in accesses if not a.expired} users: list[tuple[User, UserGroup]] = ( - User.query.join(UserGroup, User.groups) - .filter(UserGroup.id.in_(group_ids)) - .with_entities(User, UserGroup) + db.session.execute( + select(User, UserGroup) + .join(UserGroup, User.groups) + .filter(UserGroup.id.in_(group_ids)) + ) + .scalars() .all() ) groups_that_need_access_check = { @@ -770,7 +774,13 @@ def render_doc_view( flash(str(e)) ugs_without_access = [] if usergroups is not None: - ugs = UserGroup.query.filter(UserGroup.name.in_(usergroups)).all() + ugs = ( + db.session.execute( + select(UserGroup).filter(UserGroup.name.in_(usergroups)) + ) + .scalars() + .all() + ) if len(ugs) != len(usergroups): not_found_ugs = set(usergroups) - set(ug.name for ug in ugs) flash(f"Following groups were not found: {not_found_ugs}") @@ -1174,8 +1184,12 @@ def get_linked_groups(i: Item) -> tuple[list[UserGroupWithSisuInfo], list[str]]: list( map( UserGroupWithSisuInfo, - get_usergroup_eager_query() - .filter(UserGroup.name.in_(group_tags)) + db.session.execute( + get_usergroup_eager_query().filter( + UserGroup.name.in_(group_tags) + ) + ) + .scalars() .all(), ) ), diff --git a/timApp/item/routes_tags.py b/timApp/item/routes_tags.py index 6e8691adc2..434347886f 100644 --- a/timApp/item/routes_tags.py +++ b/timApp/item/routes_tags.py @@ -5,7 +5,7 @@ from datetime import datetime from flask import request, Response -from sqlalchemy import func +from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload from sqlalchemy.orm.exc import UnmappedInstanceError, FlushError # type: ignore @@ -151,9 +151,13 @@ def edit_tag(doc: str, old_tag: TagInfo, new_tag: TagInfo) -> Response: new_tag_name = new_tag.name.split(",", 1)[0] new_tag_obj = Tag(name=new_tag_name, expires=new_tag.expires, type=new_tag.type) - old_tag_obj = Tag.query.filter_by( - block_id=d.id, name=old_tag.name, type=old_tag.type - ).first() + old_tag_obj = ( + db.session.execute( + select(Tag).filter_by(block_id=d.id, name=old_tag.name, type=old_tag.type) + ) + .scalars() + .first() + ) if not old_tag_obj: raise RouteException("Tag to edit not found.") @@ -182,7 +186,13 @@ def remove_tag(doc: str, tag: TagInfo) -> Response: raise NotExist() verify_manage_access(d) - tag_obj = Tag.query.filter_by(block_id=d.id, name=tag.name, type=tag.type).first() + tag_obj = ( + db.session.execute( + select(Tag).filter_by(block_id=d.id, name=tag.name, type=tag.type) + ) + .scalars() + .first() + ) if not tag_obj: raise RouteException("Tag not found.") @@ -218,7 +228,7 @@ def get_all_tags() -> Response: of expiration. :returns The list of all unique tag names as list of strings. """ - tags = Tag.query.all() + tags = db.session.execute(select(Tag)).scalars().all() tags_unique = set() for tag in tags: @@ -244,30 +254,30 @@ def get_tagged_documents() -> Response: if exact_search: if case_sensitive: custom_filter = DocEntry.id.in_( - Tag.query.filter_by(name=tag_name).with_entities(Tag.block_id) + select(Tag.block_id).filter_by(name=tag_name) ) else: custom_filter = DocEntry.id.in_( - Tag.query.filter( + select(Tag.block_id).filter( func.lower(Tag.name) == func.lower(tag_name) - ).with_entities(Tag.block_id) + ) ) else: tag_name = f"%{tag_name}%" if case_sensitive: custom_filter = DocEntry.id.in_( - Tag.query.filter( + select(Tag.block_id).filter( Tag.name.like(tag_name) & ((Tag.expires > datetime.now()) | (Tag.expires == None)) - ).with_entities(Tag.block_id) + ) ) else: custom_filter = DocEntry.id.in_( - Tag.query.filter( + select(Tag.block_id).filter( Tag.name.ilike(tag_name) & ((Tag.expires > datetime.now()) | (Tag.expires == None)) - ).with_entities(Tag.block_id) + ) ) if list_doc_tags: diff --git a/timApp/item/taskblock.py b/timApp/item/taskblock.py index 9ab20abec3..40cf45e8a2 100644 --- a/timApp/item/taskblock.py +++ b/timApp/item/taskblock.py @@ -1,13 +1,13 @@ from __future__ import annotations +from sqlalchemy import select -from timApp.timdb.sqa import db from timApp.item.block import Block, BlockType, insert_block +from timApp.timdb.sqa import db from timApp.user.usergroup import UserGroup class TaskBlock(db.Model): - __tablename__ = "taskblock" id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) task_id = db.Column(db.Text, primary_key=True) @@ -16,11 +16,19 @@ class TaskBlock(db.Model): @staticmethod def get_by_task(task_id: str) -> TaskBlock | None: - return TaskBlock.query.filter_by(task_id=task_id).first() + return ( + db.session.execute(select(TaskBlock).filter_by(task_id=task_id)) + .scalars() + .first() + ) @staticmethod def get_block_by_task(task_id: str) -> Block | None: - task_block = TaskBlock.query.filter_by(task_id=task_id).first() + task_block = ( + db.session.execute(select(TaskBlock).filter_by(task_id=task_id)) + .scalars() + .first() + ) if task_block is not None: return task_block.block else: diff --git a/timApp/lecture/askedjson.py b/timApp/lecture/askedjson.py index 8613cadfa5..71d85de3ef 100644 --- a/timApp/lecture/askedjson.py +++ b/timApp/lecture/askedjson.py @@ -2,6 +2,8 @@ from copy import deepcopy from typing import Any +from sqlalchemy import select + from timApp.timdb.sqa import db @@ -27,7 +29,11 @@ def to_json(self, hide_points=False): def get_asked_json_by_hash(json_hash: str) -> AskedJson | None: - return AskedJson.query.filter_by(hash=json_hash).first() + return ( + db.session.execute(select(AskedJson).filter_by(hash=json_hash)) + .scalars() + .first() + ) # NOTE: Do NOT add more fields here for new qst attributes. These are ONLY for backward compatibility. diff --git a/timApp/lecture/askedquestion.py b/timApp/lecture/askedquestion.py index af56c2ad30..b972002ecc 100644 --- a/timApp/lecture/askedquestion.py +++ b/timApp/lecture/askedquestion.py @@ -2,7 +2,7 @@ from contextlib import contextmanager from datetime import timedelta, datetime -from sqlalchemy import func +from sqlalchemy import func, select from timApp.lecture.askedjson import AskedJson from timApp.lecture.lecture import Lecture @@ -136,12 +136,12 @@ def is_running(self): def get_asked_question(asked_id: int) -> AskedQuestion | None: - return AskedQuestion.query.get(asked_id) + return db.session.get(AskedQuestion, asked_id) @contextmanager def user_activity_lock(user: UserType): - db.session.query(func.pg_advisory_xact_lock(user.id)).all() + db.session.execute(select(func.pg_advisory_xact_lock(user.id))) yield return # db.session.query(func.pg_advisory_lock(user.id)).all() diff --git a/timApp/lecture/lecture.py b/timApp/lecture/lecture.py index 83fb830cab..aa703b9fdb 100644 --- a/timApp/lecture/lecture.py +++ b/timApp/lecture/lecture.py @@ -2,6 +2,8 @@ from datetime import datetime, timezone from typing import Optional +from sqlalchemy import select, func + from timApp.lecture.lectureusers import LectureUsers from timApp.timdb.sqa import db from timApp.util.utils import get_current_time @@ -38,11 +40,17 @@ class Lecture(db.Model): @staticmethod def find_by_id(lecture_id: int) -> Optional["Lecture"]: - return Lecture.query.get(lecture_id) + return db.session.get(Lecture, lecture_id) @staticmethod def find_by_code(lecture_code: str, doc_id: int) -> Optional["Lecture"]: - return Lecture.query.filter_by(lecture_code=lecture_code, doc_id=doc_id).first() + return ( + db.session.execute( + select(Lecture).filter_by(lecture_code=lecture_code, doc_id=doc_id) + ) + .scalars() + .first() + ) @staticmethod def get_all_in_document( @@ -51,9 +59,13 @@ def get_all_in_document( if not time: time = datetime.min.replace(tzinfo=timezone.utc) return ( - Lecture.query.filter_by(doc_id=doc_id) - .filter(Lecture.end_time > time) - .order_by(Lecture.lecture_code.asc()) + db.session.execute( + select(Lecture) + .filter_by(doc_id=doc_id) + .filter(Lecture.end_time > time) + .order_by(Lecture.lecture_code.asc()) + ) + .scalars() .all() ) @@ -78,7 +90,11 @@ def is_full(self): max_students = self.max_students if max_students is None: return False - cnt = LectureUsers.query.filter_by(lecture_id=self.lecture_id).count() + cnt = db.session.scalar( + select(func.count()) + .select_from(LectureUsers) + .filter_by(lecture_id=self.lecture_id) + ) return cnt >= max_students @property diff --git a/timApp/lecture/lectureanswer.py b/timApp/lecture/lectureanswer.py index 48c871ae4a..1808bfd298 100644 --- a/timApp/lecture/lectureanswer.py +++ b/timApp/lecture/lectureanswer.py @@ -2,7 +2,7 @@ from json import JSONDecodeError from typing import Optional -from sqlalchemy import func +from sqlalchemy import func, select from sqlalchemy.orm import lazyload from timApp.lecture.lecture import Lecture @@ -45,7 +45,7 @@ class LectureAnswer(db.Model): @staticmethod def get_by_id(ans_id: int) -> Optional["LectureAnswer"]: - return LectureAnswer.query.get(ans_id) + return db.session.get(LectureAnswer, ans_id) def get_parsed_answer(self): # If lecture question's rows are randomized, it will be saved as a dict @@ -86,15 +86,15 @@ def to_json(self, include_question=True, include_user=True): def get_totals( lecture: Lecture, user: User | None = None ) -> list[tuple[User, float, int]]: - q = User.query + stmt = select(User) if user: - q = q.filter_by(id=user.id) - q = ( - q.join(LectureAnswer) + stmt = stmt.filter_by(id=user.id) + stmt = ( + stmt.join(LectureAnswer) .options(lazyload(User.groups)) .filter_by(lecture_id=lecture.lecture_id) .group_by(User.id) .order_by(User.name) .with_entities(User, func.sum(LectureAnswer.points), func.count()) ) - return q.all() + return db.session.execute(stmt).scalars().all() diff --git a/timApp/lecture/routes.py b/timApp/lecture/routes.py index 9712e4d6cb..92d28e2f10 100644 --- a/timApp/lecture/routes.py +++ b/timApp/lecture/routes.py @@ -10,7 +10,7 @@ from flask import current_app from flask import request from flask import session -from sqlalchemy import func +from sqlalchemy import func, select, delete from sqlalchemy.exc import OperationalError from sqlalchemy.orm import joinedload from sqlalchemy.orm.exc import StaleDataError @@ -595,7 +595,11 @@ def get_lecture_users(lecture: Lecture): lecturers = [] students = [] - activity = Useractivity.query.filter_by(lecture=lecture).all() + activity = ( + db.session.execute(select(Useractivity).filter_by(lecture=lecture)) + .scalars() + .all() + ) cur_time = get_current_time() for ac in activity: @@ -718,23 +722,27 @@ def clean_dictionaries_by_lecture(lecture: Lecture): stop_showing_points(lecture) for a in lecture.useractivity: db.session.delete(a) - QuestionActivity.query.filter( - ( - QuestionActivity.asked_id.in_( - AskedQuestion.query.filter_by( - lecture_id=lecture.lecture_id - ).with_entities(AskedQuestion.asked_id) + db.session.execute( + delete(QuestionActivity) + .where( + ( + QuestionActivity.asked_id.in_( + select(AskedQuestion.asked_id).filter_by( + lecture_id=lecture.lecture_id + ) + ) + ) + & QuestionActivity.kind.in_( + [ + QuestionActivityKind.Usershown, + QuestionActivityKind.Pointsshown, + QuestionActivityKind.Pointsclosed, + QuestionActivityKind.Useranswered, + ] ) ) - & QuestionActivity.kind.in_( - [ - QuestionActivityKind.Usershown, - QuestionActivityKind.Pointsshown, - QuestionActivityKind.Pointsclosed, - QuestionActivityKind.Useranswered, - ] - ) - ).delete(synchronize_session="fetch") + .execution_options(synchronize_session="fetch") + ) def delete_question_temp_data(question: AskedQuestion, lecture: Lecture): @@ -747,7 +755,9 @@ def delete_question_temp_data(question: AskedQuestion, lecture: Lecture): QuestionActivityKind.Pointsshown, ], ) - Runningquestion.query.filter_by(lecture_id=lecture.lecture_id).delete() + db.session.execute( + delete(Runningquestion).where(Runningquestion.lecture_id == lecture.lecture_id) + ) stop_showing_points(lecture) @@ -773,9 +783,8 @@ def delete_lecture(m: DeleteLectureModel): lecture = get_lecture_from_request(lecture_id=m.lecture_id) with db.session.no_autoflush: empty_lecture(lecture) - Message.query.filter_by(lecture_id=lecture.lecture_id).delete() - LectureAnswer.query.filter_by(lecture_id=lecture.lecture_id).delete() - AskedQuestion.query.filter_by(lecture_id=lecture.lecture_id).delete() + for t in (Message, LectureAnswer, AskedQuestion): + db.session.execute(delete(t).where(t.lecture_id == lecture.lecture_id)) db.session.delete(lecture) db.session.commit() @@ -915,7 +924,7 @@ def ask_question(): if not doc_id: raise RouteException("doc_id missing") if question_id: - question = Question.query.get(question_id) # Old version??? + question = db.session.get(Question, question_id) # Old version??? question_json_str = question.questionjson markup = json.loads(question_json_str) else: @@ -990,13 +999,15 @@ def show_points(m: ShowAnswerPointsModel): def stop_showing_points(lecture: Lecture): - Showpoints.query.filter( - Showpoints.asked_id.in_( - AskedQuestion.query.filter_by(lecture_id=lecture.lecture_id).with_entities( - AskedQuestion.asked_id + db.session.execute( + delete(Showpoints) + .where( + Showpoints.asked_id.in_( + select(AskedQuestion.asked_id).filter_by(lecture_id=lecture.lecture_id) ) ) - ).delete(synchronize_session="fetch") + .execution_options(synchronize_session="fetch") + ) @lecture_routes.post("/updatePoints/") @@ -1023,10 +1034,14 @@ def update_question_points(): def delete_activity(question: AskedQuestion, kinds): - QuestionActivity.query.filter( - (QuestionActivity.asked_id == question.asked_id) - & QuestionActivity.kind.in_(kinds) - ).delete(synchronize_session="fetch") + db.session.execute( + delete(QuestionActivity) + .where( + (QuestionActivity.asked_id == question.asked_id) + & QuestionActivity.kind.in_(kinds) + ) + .execution_options(synchronize_session="fetch") + ) @lecture_routes.get("/getQuestionByParId") diff --git a/timApp/messaging/messagelist/emaillist.py b/timApp/messaging/messagelist/emaillist.py index e03563562e..e0ebf17541 100644 --- a/timApp/messaging/messagelist/emaillist.py +++ b/timApp/messaging/messagelist/emaillist.py @@ -5,6 +5,7 @@ from mailmanclient import Client, MailingList, Domain, Member from mailmanclient.restbase.connection import Connection from marshmallow import EXCLUDE +from sqlalchemy import select, delete from timApp.messaging.messagelist.listinfo import ( ListInfo, @@ -777,7 +778,6 @@ def update_mailing_list_address(old: str, new: str) -> None: if not check_mailman_connection(): return try: - usr = _client.get_user(old) addr = usr.add_address(new, absorb_existing=True) addr.verify() @@ -798,20 +798,24 @@ def update_mailing_list_address(old: str, new: str) -> None: # TODO: This is irreversible, i.e. user changing primary email back doesn't restore old external membership for old_member, new_member in member_pairs: - delete_ids_q = ( - db.session.query(MessageListExternalMember.id) + delete_ids_stmt = ( + select(MessageListExternalMember.id) .join(MessageListModel) .filter( (MessageListExternalMember.email_address == new) & (MessageListModel.mailman_list_id == new_member.list_id) ) - ).all() - MessageListExternalMember.query.filter( - MessageListExternalMember.id.in_(delete_ids_q) - ).delete(synchronize_session=False) - MessageListMember.query.filter( - MessageListMember.id.in_(delete_ids_q) - ).delete(synchronize_session=False) + ) + db.session.execute( + delete(MessageListExternalMember) + .where(MessageListExternalMember.id.in_(delete_ids_stmt)) + .execution_options(synchronize_session=False) + ) + db.session.execute( + delete(MessageListMember) + .where(MessageListMember.id.in_(delete_ids_stmt)) + .execution_options(synchronize_session=False) + ) new_member.unsubscribe() for member in old_members.values(): diff --git a/timApp/messaging/messagelist/messagelist_models.py b/timApp/messaging/messagelist/messagelist_models.py index 89d0737f08..0b5a342727 100644 --- a/timApp/messaging/messagelist/messagelist_models.py +++ b/timApp/messaging/messagelist/messagelist_models.py @@ -2,6 +2,7 @@ from enum import Enum from typing import Optional, Any +from sqlalchemy import select from sqlalchemy.ext.hybrid import hybrid_property # type: ignore from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound # type: ignore @@ -115,13 +116,21 @@ class MessageListModel(db.Model): @staticmethod def get_by_email(email: str) -> Optional["MessageListModel"]: name, domain = email.split("@", 1) - return MessageListModel.query.filter_by( - name=name, email_list_domain=domain - ).first() + return ( + db.session.execute( + select(MessageListModel).filter_by(name=name, email_list_domain=domain) + ) + .scalars() + .first() + ) @staticmethod def from_manage_doc_id(doc_id: int) -> "MessageListModel": - return MessageListModel.query.filter_by(manage_doc_id=doc_id).one() + return ( + db.session.execute(select(MessageListModel).filter_by(manage_doc_id=doc_id)) + .scalars() + .one() + ) @staticmethod def from_name(name: str) -> "MessageListModel": @@ -144,7 +153,11 @@ def get_by_name(name_candidate: str) -> Optional["MessageListModel"]: :param name_candidate: The name of the message list. :return: Return the message list after query by name. Returns at most one result or None if no there are hits. """ - return MessageListModel.query.filter_by(name=name_candidate).first() + return ( + db.session.execute(select(MessageListModel).filter_by(name=name_candidate)) + .scalars() + .first() + ) @staticmethod def name_exists(name_candidate: str) -> bool: @@ -153,8 +166,10 @@ def name_exists(name_candidate: str) -> bool: :param name_candidate: The name we are checking if it already is already in use by another list. """ return ( - db.session.query(MessageListModel.name) - .filter_by(name=name_candidate) + db.session.execute( + select(MessageListModel.name).filter_by(name=name_candidate).limit(1) + ) + .scalars() .first() is not None ) @@ -322,7 +337,7 @@ def is_personal_user(self) -> bool: return self.is_external_member() from timApp.user.usergroup import UserGroup - ug = UserGroup.query.filter_by(id=gid).one() + ug = db.session.exectute(select(UserGroup).filter_by(id=gid)).scalars().one() return ug.is_personal_group def is_group(self) -> bool: diff --git a/timApp/messaging/messagelist/messagelist_utils.py b/timApp/messaging/messagelist/messagelist_utils.py index 46b3e4f251..5ed567f9ed 100644 --- a/timApp/messaging/messagelist/messagelist_utils.py +++ b/timApp/messaging/messagelist/messagelist_utils.py @@ -13,6 +13,7 @@ from flask import render_template_string from isodate import datetime_isoformat from mailmanclient import MailingList +from sqlalchemy import select from sqlalchemy.orm import load_only from timApp.auth.accesshelper import has_manage_access, AccessDenied @@ -694,7 +695,11 @@ def get_message_list_owners(mlist: MessageListModel) -> list[UserGroup]: :param mlist: The message list we want to know the owners. :return: A list of owners, as their personal user group. """ - manage_doc_block = Block.query.filter_by(id=mlist.manage_doc_id).one() + manage_doc_block = ( + db.session.execute(select(Block).filter_by(id=mlist.manage_doc_id)) + .scalars() + .one() + ) return manage_doc_block.owners @@ -1302,13 +1307,21 @@ def sync_usergroup_messagelist_members( if not user_ids: return - user_query = User.query.filter(User.id.in_(user_ids)).options( - load_only(User.id, User.email, User.real_name) + user_stmt = ( + select(User) + .filter(User.id.in_(user_ids)) + .options(load_only(User.id, User.email, User.real_name)) ) - users = {user.id: user for user in user_query} + users = {user.id: user for user in db.session.execute(user_stmt).scalars()} try: for ug_id, diff in diffs.items(): - ug_memberships = MessageListTimMember.query.filter_by(group_id=ug_id).all() + ug_memberships = ( + db.session.execute( + select(MessageListTimMember).filter_by(group_id=ug_id) + ) + .scalars() + .all() + ) for group_tim_member in ug_memberships: group_message_list: MessageListModel = group_tim_member.message_list if group_message_list.email_list_domain: diff --git a/timApp/messaging/timMessage/internalmessage_models.py b/timApp/messaging/timMessage/internalmessage_models.py index a7fa88641b..24f9935e8b 100644 --- a/timApp/messaging/timMessage/internalmessage_models.py +++ b/timApp/messaging/timMessage/internalmessage_models.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Any, Optional, TYPE_CHECKING -from sqlalchemy import func +from sqlalchemy import func, select from timApp.timdb.sqa import db @@ -127,9 +127,13 @@ class InternalMessageReadReceipt(db.Model): def get_for_user( user: "User", message: InternalMessage ) -> Optional["InternalMessageReadReceipt"]: - return InternalMessageReadReceipt.query.filter_by( - user=user, message=message - ).first() + return ( + db.session.execute( + select(InternalMessageReadReceipt).filter_by(user=user, message=message) + ) + .scalars() + .first() + ) def to_json(self) -> dict[str, Any]: return { diff --git a/timApp/messaging/timMessage/routes.py b/timApp/messaging/timMessage/routes.py index 3903bcae65..15a0ffff9e 100644 --- a/timApp/messaging/timMessage/routes.py +++ b/timApp/messaging/timMessage/routes.py @@ -5,7 +5,7 @@ from flask import Response from isodate import datetime_isoformat -from sqlalchemy import tuple_ +from sqlalchemy import tuple_, select from sqlalchemy.orm import contains_eager from timApp.auth.accesshelper import ( @@ -144,9 +144,12 @@ def expire_tim_message(message_doc_id: int) -> Response: :param message_doc_id: Document ID of the message to expire. :return: OK response if message was successfully expired. """ - internal_message: InternalMessage | None = InternalMessage.query.filter_by( - doc_id=message_doc_id - ).first() + + internal_message: InternalMessage | None = ( + db.session.execute(select(InternalMessage).filter_by(doc_id=message_doc_id)) + .scalars() + .first() + ) if not internal_message: raise NotExist("Message not found") verify_manage_access(internal_message.block) @@ -197,8 +200,9 @@ def get_tim_messages_as_list(item_id: int | None = None) -> list[TimMessageData] ) cur_user = get_current_user_object() - q = ( - InternalMessage.query.join(InternalMessageDisplay) + stmt = ( + select(InternalMessage) + .join(InternalMessageDisplay) .outerjoin(Folder, Folder.id == InternalMessageDisplay.display_doc_id) .outerjoin( InternalMessageReadReceipt, @@ -211,7 +215,7 @@ def get_tim_messages_as_list(item_id: int | None = None) -> list[TimMessageData] .filter((is_global | is_user_specific) & can_see) ) - messages: list[InternalMessage] = q.all() + messages: list[InternalMessage] = db.session.execute(stmt).scalars().all() full_messages = [] for message in messages: @@ -264,7 +268,11 @@ def get_read_receipt(doc_id: int) -> Response: :param doc_id: Id of the message document :return: """ - message = InternalMessage.query.filter_by(doc_id=doc_id).first() + message = ( + db.session.execute(select(InternalMessage).filter_by(doc_id=doc_id)) + .scalars() + .first() + ) if not message: raise NotExist("No active messages for the document found") receipt = InternalMessageReadReceipt.get_for_user( @@ -486,7 +494,11 @@ def mark_as_read(message_id: int) -> Response: """ verify_logged_in() - message = InternalMessage.query.filter_by(id=message_id).first() + message = ( + db.session.execute(select(InternalMessage).filter_by(id=message_id)) + .scalars() + .first() + ) if not message: raise NotExist("Message not found by the ID") read_receipt = InternalMessageReadReceipt.get_for_user( @@ -514,9 +526,15 @@ def cancel_read_receipt(message_id: int) -> Response: """ verify_logged_in() - receipt = InternalMessageReadReceipt.query.filter_by( - user_id=get_current_user_object().id, message_id=message_id - ).first() + receipt = ( + db.session.execute( + select(InternalMessageReadReceipt).filter_by( + user_id=get_current_user_object().id, message_id=message_id + ) + ) + .scalars() + .first() + ) if not receipt: raise NotExist("No read receipt found for the message") receipt.marked_as_read_on = None @@ -547,8 +565,8 @@ def get_read_receipts( raise NotExist("No document found") verify_manage_access(doc) - read_users = ( - db.session.query( + read_users = db.session.execute( + select( InternalMessageReadReceipt.user_id, InternalMessageReadReceipt.marked_as_read_on, InternalMessageReadReceipt.last_seen, @@ -565,21 +583,30 @@ def get_read_receipts( } all_recipients = ( - User.query.join(UserGroupMember, User.active_memberships) - .join( - InternalMessageDisplay, - InternalMessageDisplay.usergroup_id == UserGroupMember.usergroup_id, + db.session.execute( + select(User) + .join(UserGroupMember, User.active_memberships) + .join( + InternalMessageDisplay, + InternalMessageDisplay.usergroup_id == UserGroupMember.usergroup_id, + ) + .join(InternalMessage) + .filter(InternalMessage.doc_id == doc.id) ) - .join(InternalMessage) - .filter(InternalMessage.doc_id == doc.id) - ).all() + .scalars() + .all() + ) if not all_recipients: if include_unread: raise RouteException( "For performance reasons, only read users can be shown for global messages" ) - all_recipients = User.query.filter(User.id.in_(read_user_map.keys())).all() + all_recipients = ( + db.session.execute(select(User).filter(User.id.in_(read_user_map.keys()))) + .scalars() + .all() + ) if receipt_format == ReadReceiptFormat.Count: count_data = [ @@ -641,11 +668,15 @@ def get_recipient_users(recipients: list[str] | None) -> list[UserGroup]: if user := User.get_by_email(rcpt): users.add(UserGroup.get_by_name(user.name)) if msg_list := MessageListModel.get_by_email(rcpt): - q = UserGroup.query.join(MessageListTimMember).filter( - (MessageListTimMember.message_list == msg_list) - & (MessageListTimMember.membership_ended == None) + stmt = ( + select(UserGroup) + .join(MessageListTimMember) + .filter( + (MessageListTimMember.message_list == msg_list) + & (MessageListTimMember.membership_ended == None) + ) ) - ugs = q.all() + ugs = db.session.execute(stmt).scalars().all() users.update(ugs) return list(users) diff --git a/timApp/migrations/versions/422ab312e579_membership_end_added_by.py b/timApp/migrations/versions/422ab312e579_membership_end_added_by.py index 8641c3ce92..d6c3cc3357 100644 --- a/timApp/migrations/versions/422ab312e579_membership_end_added_by.py +++ b/timApp/migrations/versions/422ab312e579_membership_end_added_by.py @@ -7,6 +7,7 @@ """ from typing import Any +from sqlalchemy import select from sqlalchemy.orm import scoped_session, sessionmaker from timApp.sisu.scimusergroup import ScimUserGroup @@ -33,12 +34,13 @@ def upgrade(): tmp: Any = scoped_session(session_factory=sessionmaker(bind=bind)) db.session = tmp ugs: list[tuple[UserGroup, ScimUserGroup]] = ( - UserGroup.query.join(ScimUserGroup) - .with_entities(UserGroup, ScimUserGroup) + db.session.execute(select(UserGroup, ScimUserGroup).join(ScimUserGroup)) + .scalars() .all() ) su = User.get_scimuser() for ug, sg in ugs: + # FIXME: SQLAlchemy dynamic ms = ( ug.memberships.filter( UserGroupMember.user_id.in_( diff --git a/timApp/migrations/versions/c0d6e136511e_make_special_folders_lowercase.py b/timApp/migrations/versions/c0d6e136511e_make_special_folders_lowercase.py index e454577d13..f569be0f6a 100644 --- a/timApp/migrations/versions/c0d6e136511e_make_special_folders_lowercase.py +++ b/timApp/migrations/versions/c0d6e136511e_make_special_folders_lowercase.py @@ -5,6 +5,7 @@ Create Date: 2017-10-10 10:48:20.601314 """ +from sqlalchemy import select # revision identifiers, used by Alembic. from timApp.folder.folder import Folder @@ -15,26 +16,26 @@ def upgrade(): - for f in Folder.query.all(): + for f in db.session.execute(select(Folder)).scalars().all(): if f.path.endswith("/Templates/printing") or f.path == "Templates/printing": f.rename("printing_old") - for f in Folder.query.all(): + for f in db.session.execute(select(Folder)).scalars().all(): if f.name == "templates": f.rename("templates_old") - for f in Folder.query.all(): + for f in db.session.execute(select(Folder)).scalars().all(): if f.path.endswith("/Templates/Printing") or f.path == "Templates/Printing": f.rename("printing") - for f in Folder.query.all(): + for f in db.session.execute(select(Folder)).scalars().all(): if f.name == "Templates": f.rename("templates") db.session.commit() def downgrade(): - for f in Folder.query.all(): + for f in db.session.execute(select(Folder)).scalars().all(): if f.path.endswith("/templates/printing") or f.path == "templates/printing": f.rename("Printing") - for f in Folder.query.all(): + for f in db.session.execute(select(Folder)).scalars().all(): if f.name == "templates": f.rename("Templates") db.session.commit() diff --git a/timApp/migrations/versions/ef104a711321_add_scimusergroup_table.py b/timApp/migrations/versions/ef104a711321_add_scimusergroup_table.py index 4d5e5fe13d..04c4a35ef6 100644 --- a/timApp/migrations/versions/ef104a711321_add_scimusergroup_table.py +++ b/timApp/migrations/versions/ef104a711321_add_scimusergroup_table.py @@ -8,7 +8,7 @@ # revision identifiers, used by Alembic. -from sqlalchemy import orm, any_ +from sqlalchemy import orm, any_, select from timApp.sisu.scim import derive_scim_group_name from timApp.sisu.scimusergroup import ScimUserGroup @@ -37,7 +37,9 @@ def upgrade(): s = orm.Session(bind=bind) ugs: list[UserGroup] = ( - s.query(UserGroup).filter(UserGroup.name.startswith("sisu:")).all() + s.execute(select(UserGroup).filter(UserGroup.name.startswith("sisu:"))) + .scalars() + .all() ) used_names = set() for ug in ugs: @@ -50,8 +52,12 @@ def upgrade(): ug.name = default_name ugs: list[UserGroup] = ( - s.query(UserGroup) - .filter(UserGroup.name.like(any_(["deleted:sisu:%", "cumulative:sisu:%"]))) + s.execute( + select(UserGroup).filter( + UserGroup.name.like(any_(["deleted:sisu:%", "cumulative:sisu:%"])) + ) + ) + .scalars() .all() ) for ug in ugs: diff --git a/timApp/note/notes.py b/timApp/note/notes.py index 0906d1e9d9..7208c8074c 100644 --- a/timApp/note/notes.py +++ b/timApp/note/notes.py @@ -1,11 +1,13 @@ from typing import NamedTuple +from sqlalchemy import select from sqlalchemy.orm import lazyload from timApp.document.docparagraph import DocParagraph from timApp.document.document import Document from timApp.markdown.markdownconverter import md_to_html from timApp.note.usernote import UserNote +from timApp.timdb.sqa import db from timApp.user.user import User from timApp.user.usergroup import UserGroup @@ -56,16 +58,16 @@ def get_notes( f = UserGroup.id == usergroup_id if include_public: f = f | (UserNote.access == "everyone") - q = ( - UserNote.query.filter(UserNote.doc_id.in_(ids)) + stmt = ( + select(UserNote, User) + .filter(UserNote.doc_id.in_(ids)) .join(UserGroup) .join(User, User.name == UserGroup.name) .options(lazyload("*")) .filter(f) .order_by(UserNote.id) - .with_entities(UserNote, User) ) - return process_notes(q.all()) + return process_notes(db.session.execute(stmt).scalars().all()) def move_notes(src_par: DocParagraph, dest_par: DocParagraph): @@ -78,8 +80,8 @@ def move_notes(src_par: DocParagraph, dest_par: DocParagraph): ) == str(dest_par.get_id()): return - for u in UserNote.query.filter_by( - doc_id=src_par.doc.doc_id, par_id=src_par.get_id() - ): + for u in db.session.execute( + select(UserNote).filter_by(doc_id=src_par.doc.doc_id, par_id=src_par.get_id()) + ).scalars(): u.doc_id = dest_par.doc.doc_id u.par_id = dest_par.get_id() diff --git a/timApp/note/routes.py b/timApp/note/routes.py index 9d08b671fc..0f67c93e0f 100644 --- a/timApp/note/routes.py +++ b/timApp/note/routes.py @@ -3,7 +3,7 @@ from typing import Any from flask import Response -from sqlalchemy import true +from sqlalchemy import true, select from sqlalchemy.orm import joinedload from timApp.auth.accesshelper import ( @@ -143,11 +143,13 @@ def get_notes( time_restriction = time_restriction & (UserNote.created < end) d_ids = [d.id for d in docs] ns = ( - UserNote.query.filter( - UserNote.doc_id.in_(d_ids) & access_restriction & time_restriction + db.session.execute( + select(UserNote) + .filter(UserNote.doc_id.in_(d_ids) & access_restriction & time_restriction) + .options(joinedload(UserNote.usergroup)) + .options(joinedload(UserNote.block).joinedload(Block.docentries)) ) - .options(joinedload(UserNote.usergroup)) - .options(joinedload(UserNote.block).joinedload(Block.docentries)) + .scalars() .all() ) all_count = len(ns) @@ -157,13 +159,19 @@ def get_notes( deleted_notes = list( map( DeletedNote, - PendingNotification.query.filter( - PendingNotification.doc_id.in_(d_ids) - & (PendingNotification.kind == NotificationType.CommentDeleted) - ) - .options( - joinedload(PendingNotification.block).joinedload(Block.docentries) + db.session.execute( + select(PendingNotification) + .filter( + PendingNotification.doc_id.in_(d_ids) + & (PendingNotification.kind == NotificationType.CommentDeleted) + ) + .options( + joinedload(PendingNotification.block).joinedload( + Block.docentries + ) + ) ) + .scalars() .all(), ) ) diff --git a/timApp/note/usernote.py b/timApp/note/usernote.py index 47d6783b06..4f818b4bfb 100644 --- a/timApp/note/usernote.py +++ b/timApp/note/usernote.py @@ -1,5 +1,3 @@ -from typing import Optional - from sqlalchemy import func from timApp.timdb.sqa import db @@ -68,4 +66,4 @@ def to_json(self): def get_comment_by_id(c_id: int) -> UserNote | None: - return UserNote.query.get(c_id) + return db.session.get(UserNote, c_id) diff --git a/timApp/notification/notify.py b/timApp/notification/notify.py index 343260e814..e79f5030dd 100644 --- a/timApp/notification/notify.py +++ b/timApp/notification/notify.py @@ -5,6 +5,7 @@ from typing import DefaultDict, Callable from flask import current_app +from sqlalchemy import select from sqlalchemy.orm import joinedload from timApp.auth.accesshelper import ( @@ -81,15 +82,18 @@ def get_user_notify_settings(): def get_current_user_notifications(limit: int | None = None): - q = ( - Notification.query.filter_by(user_id=get_current_user_id()) + stmt = ( + select(Notification) + .filter_by(user_id=get_current_user_id()) .options(joinedload(Notification.block).joinedload(Block.docentries)) .options(joinedload(Notification.block).joinedload(Block.folder)) .options(joinedload(Notification.block).joinedload(Block.translation)) - ).order_by(Notification.block_id.desc()) + .order_by(Notification.block_id.desc()) + ) + if limit is not None: - q = q.limit(limit) - nots = q.all() + stmt = stmt.limit(limit) + nots = db.session.execute(stmt).scalars().all() return nots diff --git a/timApp/notification/pending_notification.py b/timApp/notification/pending_notification.py index 1868d008c3..b25be4079d 100644 --- a/timApp/notification/pending_notification.py +++ b/timApp/notification/pending_notification.py @@ -1,4 +1,4 @@ -from sqlalchemy import func +from sqlalchemy import func, select from timApp.document.version import Version from timApp.notification.notification import NotificationType @@ -88,7 +88,11 @@ def grouping_key(self) -> GroupingKey: def get_pending_notifications() -> list[PendingNotification]: return ( - PendingNotification.query.filter(PendingNotification.processed == None) - .order_by(PendingNotification.created.asc()) + db.session.execute( + select(PendingNotification) + .filter(PendingNotification.processed == None) + .order_by(PendingNotification.created.asc()) + ) + .scalars() .all() ) diff --git a/timApp/peerreview/util/groups.py b/timApp/peerreview/util/groups.py index 562388ac38..dd4f39a272 100644 --- a/timApp/peerreview/util/groups.py +++ b/timApp/peerreview/util/groups.py @@ -3,11 +3,12 @@ from random import shuffle from typing import DefaultDict +from sqlalchemy import select + from timApp.answer.answer import Answer from timApp.answer.answers import get_points_by_rule, get_latest_answers_query from timApp.document.docinfo import DocInfo from timApp.peerreview.peerreview import PeerReview -from timApp.plugin.plugin import Plugin from timApp.plugin.taskid import TaskId from timApp.timdb.sqa import db from timApp.user.user import User @@ -23,12 +24,11 @@ def generate_review_groups(doc: DocInfo, task_ids: list[TaskId]) -> None: if user_groups: user_ids = [ uid - for uid, in ( - UserGroupMember.query.join(UserGroup, UserGroupMember.group) + for uid, in db.session.execute( + select(UserGroupMember.user_id) + .join(UserGroup, UserGroupMember.group) .filter(membership_current & (UserGroup.name.in_(user_groups))) - .with_entities(UserGroupMember.user_id) - .all() - ) + ).scalars() ] valid_only = not settings.peer_review_allow_invalid() points = get_points_by_rule(None, task_ids, user_ids, show_valid_only=valid_only) @@ -82,7 +82,9 @@ def generate_review_groups(doc: DocInfo, task_ids: list[TaskId]) -> None: # PeerReview rows and pairings will be the same for every task, even if target did not answer to some of tasks # If target has an answer in a task, try to add it to PeerReview table. If not, just leave it empty for t in task_ids: - answers: list[Answer] = get_latest_answers_query(t, users, valid_only).all() + answers: list[Answer] = db.session.scalars( + get_latest_answers_query(t, users, valid_only) + ).all() excluded_users: list[User] = [] filtered_answers = [] for answer in answers: diff --git a/timApp/peerreview/util/peerreview_utils.py b/timApp/peerreview/util/peerreview_utils.py index f5f801e623..acf383414d 100644 --- a/timApp/peerreview/util/peerreview_utils.py +++ b/timApp/peerreview/util/peerreview_utils.py @@ -6,8 +6,10 @@ __license__ = "MIT" __date__ = "29.5.2022" +from sqlalchemy import select from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import joinedload, Query +from sqlalchemy.orm import joinedload +from sqlalchemy.sql import Select from timApp.document.docinfo import DocInfo from timApp.peerreview.peerreview import PeerReview @@ -20,37 +22,45 @@ def get_reviews_where_user_is_reviewer(d: DocInfo, user: User) -> list[PeerReview]: """Return all peer_review rows where block_is is d.id and the person making the review is the given user""" - q = get_reviews_where_user_is_reviewer_query(d, user).options( + stmt = get_reviews_where_user_is_reviewer_query(d, user).options( joinedload(PeerReview.reviewable) ) - return q.all() + return db.session.execute(stmt).scalars().all() -def get_reviews_where_user_is_reviewer_query(d: DocInfo, user: User) -> Query: - return PeerReview.query.filter_by(block_id=d.id, reviewer_id=user.id) +def get_reviews_where_user_is_reviewer_query(d: DocInfo, user: User) -> Select: + return select(PeerReview).filter_by(block_id=d.id, reviewer_id=user.id) def get_all_reviews(doc: DocInfo) -> list[PeerReview]: - return PeerReview.query.filter_by(block_id=doc.id).all() + return ( + db.session.execute(select(PeerReview).filter_by(block_id=doc.id)) + .scalars() + .all() + ) def get_reviews_targeting_user(d: DocInfo, user: User) -> list[PeerReview]: """Return all peer_review rows where block_id is d.id and the user is the review target""" - q = get_reviews_targeting_user_query(d, user).options( + stmt = get_reviews_targeting_user_query(d, user).options( joinedload(PeerReview.reviewable) ) - return q.all() + return db.session.execute(stmt).scalars().all() -def get_reviews_targeting_user_query(d: DocInfo, user: User) -> Query: - return PeerReview.query.filter_by(block_id=d.id, reviewable_id=user.id) +def get_reviews_targeting_user_query(d: DocInfo, user: User) -> Select: + return select(PeerReview).filter_by(block_id=d.id, reviewable_id=user.id) def get_reviews_related_to_user(d: DocInfo, user: User) -> list[PeerReview]: - q = PeerReview.query.filter_by(block_id=d.id).filter( - (PeerReview.reviewable_id == user.id) | (PeerReview.reviewer_id == user.id) + stmt = ( + select(PeerReview) + .filter_by(block_id=d.id) + .filter( + (PeerReview.reviewable_id == user.id) | (PeerReview.reviewer_id == user.id) + ) ) - return q.all() + return db.session.execute(stmt).scalars().all() def has_review_access( @@ -61,24 +71,26 @@ def has_review_access( ) -> bool: if not is_peerreview_enabled(doc): return False - q = PeerReview.query.filter_by( + stmt = select(PeerReview).filter_by( block_id=doc.id, reviewer_id=reviewer_user.id, ) if task_id is not None: - q = q.filter_by(task_name=task_id.task_name) + stmt = stmt.filter_by(task_name=task_id.task_name) if reviewable_user is not None: - q = q.filter_by(reviewable_id=reviewable_user.id) - return bool(q.first()) + stmt = stmt.filter_by(reviewable_id=reviewable_user.id) + return bool(db.session.execute(stmt.limit(1)).scalars().first()) def check_review_grouping(doc: DocInfo, tasks: list[TaskId]) -> bool: # TODO: new tasks may be added to area after pr starts # => if any tasks in area (list of tasks) has pr rows, then copy the pairings to tasks in list missing pr rows - q = PeerReview.query.filter_by(block_id=doc.id).filter( - PeerReview.task_name.in_([t.task_name for t in tasks]) + stmt = ( + select(PeerReview) + .filter_by(block_id=doc.id) + .filter(PeerReview.task_name.in_([t.task_name for t in tasks])) ) - return bool(q.first()) + return bool(db.session.execute(stmt.limit(1)).scalars().first()) def is_peerreview_enabled(doc: DocInfo) -> bool: @@ -96,9 +108,15 @@ def get_reviews_for_document(doc: DocInfo) -> list[PeerReview]: """Get peer-reviewers of current document from the database. :param doc: Document containing reviewable answers. """ - return PeerReview.query.filter_by( - block_id=doc.id, - ).all() + return ( + db.session.execute( + select(PeerReview).filter_by( + block_id=doc.id, + ) + ) + .scalars() + .all() + ) def change_peerreviewers_for_user( @@ -117,12 +135,20 @@ def change_peerreviewers_for_user( """ # TODO: Clean up; don't do one query per loop: instead fetch all users at once! for i in range(0, len(new_reviewers)): - updated_user = PeerReview.query.filter_by( - block_id=doc.id, - reviewer_id=old_reviewers[i], - reviewable_id=reviewable, - task_name=task, - ).first() + updated_user = ( + db.session.execute( + select(PeerReview) + .filter_by( + block_id=doc.id, + reviewer_id=old_reviewers[i], + reviewable_id=reviewable, + task_name=task, + ) + .limit(1) + ) + .scalars() + .first() + ) updated_user.reviewer_id = new_reviewers[i] try: db.session.flush() diff --git a/timApp/plugin/calendar/calendar.py b/timApp/plugin/calendar/calendar.py index 72adfda6ca..a1eae8ad09 100644 --- a/timApp/plugin/calendar/calendar.py +++ b/timApp/plugin/calendar/calendar.py @@ -24,7 +24,7 @@ from flask import Response, render_template_string, url_for from marshmallow import missing -from sqlalchemy import false, true, func +from sqlalchemy import false, true, func, select, update from timApp.auth.accesshelper import ( verify_logged_in, @@ -364,9 +364,13 @@ def export_ical(user: User) -> Response: :param user: User to generate ICS link for :return: """ - user_data: ExportedCalendar = ExportedCalendar.query.filter( - ExportedCalendar.user_id == user.id - ).one_or_none() + user_data: ExportedCalendar = ( + db.session.execute( + select(ExportedCalendar).filter(ExportedCalendar.user_id == user.id) + ) + .scalars() + .one_or_none() + ) if user_data is not None: hash_code = user_data.calendar_hash url = url_for("calendar_plugin.get_ical", key=hash_code, _external=True) @@ -409,9 +413,11 @@ def get_ical(opts: ICalFilterOptions) -> Response: :return: ICS file that can be exported otherwise 404 if user data does not exist. """ - user_data: ExportedCalendar = ExportedCalendar.query.filter_by( - calendar_hash=opts.key - ).one_or_none() + user_data: ExportedCalendar = ( + db.session.execute(select(ExportedCalendar).filter_by(calendar_hash=opts.key)) + .scalars() + .one_or_none() + ) if user_data is None: raise NotExist() @@ -472,7 +478,7 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev """ filter_opts = filter_opts or FilterOptions() - q = Event.query + stmt = select(Event) event_queries = [] event_filter = false() @@ -490,7 +496,7 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev subquery_event_groups_all = ( u.get_groups(include_expired=False, include_special=False) .join(EventGroup, EventGroup.usergroup_id == UserGroup.id) - .with_entities(EventGroup.event_id) + .with_only_columns(EventGroup.event_id) ) subquery_event_groups = subquery_event_groups_all # Apply group filter if there is one @@ -504,15 +510,15 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev # Filter out any tags and groups if filter_opts.tags is not None: - q = q.join(EventTag, Event.tags) + stmt = stmt.join(EventTag, Event.tags) # noinspection PyUnresolvedReferences event_filter &= EventTag.tag.in_(filter_opts.tags) - q = q.filter(event_filter) + stmt = stmt.filter(event_filter) if filter_opts.showImportant: # noinspection PyUnresolvedReferences - important_q = Event.query.filter( + important_q = select(Event).filter( Event.event_id.in_(subquery_event_groups_all) & Event.important.is_(True) ) event_queries.append(important_q) @@ -522,27 +528,28 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev enrolled_subquery = ( u.get_groups(include_expired=False) .join(Enrollment, Enrollment.usergroup_id == UserGroup.id) - .with_entities(Enrollment.event_id) + .with_only_columns(Enrollment.event_id) .subquery() ) # noinspection PyUnresolvedReferences - booked_query = Event.query.filter(Event.event_id.in_(enrolled_subquery)) + booked_query = select(Event).filter(Event.event_id.in_(enrolled_subquery)) event_queries.append(booked_query) if filter_opts.showBookedByMin is not None: booked_min_subquery = ( - Event.query.filter(Event.creator == u) + select(Event) + .filter(Event.creator == u) .outerjoin(Enrollment) .group_by(Event.event_id) - .with_entities( + .with_only_columns( Event.event_id, func.count(Enrollment.event_id).label("count") ) ).subquery() booked_min_query = ( - db.session.query(booked_min_subquery) + select(Event) + .select_from(booked_min_subquery) .join(Event, Event.event_id == booked_min_subquery.c.event_id) .filter(booked_min_subquery.c.count >= filter_opts.showBookedByMin) - .with_entities(Event) ) event_queries.append(booked_min_query) @@ -554,10 +561,10 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev timing_filter &= Event.end_time <= filter_opts.toDate if event_queries: - q = q.union(*event_queries) - q = q.filter(timing_filter) + stmt = stmt.union(*event_queries) + stmt = stmt.filter(timing_filter) - return q.all() + return db.session.execute(stmt).scalars().all() @calendar_plugin.get("/events", model=FilterOptions) @@ -694,7 +701,11 @@ def save_events( _replace_group_wildcards(event_ug_names) # noinspection PyUnresolvedReferences - event_ugs = UserGroup.query.filter(UserGroup.name.in_(event_ug_names)).all() + event_ugs = ( + db.session.execute(select(UserGroup).filter(UserGroup.name.in_(event_ug_names))) + .scalars() + .all() + ) event_ugs_dict = {ug.name: ug for ug in event_ugs} event_tags = set( [ @@ -798,7 +809,11 @@ def update_event(cal_event: CalendarEvent, event: Event) -> Event: if calendar_event.id is not missing: if not modify_existing: raise AccessDenied("Cannot modify existing events via this route") - event: Event = Event.query.filter_by(event_id=calendar_event.id).first() + event: Event = ( + db.session.execute(select(Event).filter_by(event_id=calendar_event.id)) + .scalars() + .first() + ) if not event: raise NotExist(f"Event with id {calendar_event.id} not found") rights = event.get_enrollment_right(user) @@ -949,7 +964,11 @@ def send_email_to_enrolled_users( enrolled_users = event.enrolled_users user_accounts = [] for user_group in enrolled_users: - user_account = User.query.filter(User.name == user_group.name).one_or_none() + user_account = ( + db.session.execute(select(User).filter(User.name == user_group.name)) + .scalars() + .one_or_none() + ) if user_account is None: raise NotExist() user_accounts.append(user_account) @@ -995,9 +1014,13 @@ def update_book_message(event_id: int, booker_msg: str, booker_group: str) -> Re if not enrollment: raise NotExist() - Enrollment.query.filter_by(event_id=enrollment.event_id).update( - {"booker_message": Enrollment.booker_message + f"\n{new_message}"}, - synchronize_session="fetch", + db.session.execute( + update(Enrollment) + .where(Enrollment.event_id == enrollment.event_id) + .values( + {"booker_message": Enrollment.booker_message + f"\n{new_message}"}, + ) + .execution_options(synchronize_session="fetch") ) db.session.commit() diff --git a/timApp/plugin/calendar/models.py b/timApp/plugin/calendar/models.py index d86c1adba5..5127a47890 100644 --- a/timApp/plugin/calendar/models.py +++ b/timApp/plugin/calendar/models.py @@ -15,7 +15,7 @@ from dataclasses import dataclass from typing import Optional, Iterable -from sqlalchemy import func +from sqlalchemy import func, select from timApp.timdb.sqa import db from timApp.user.user import User @@ -79,9 +79,16 @@ def get_by_event_and_user( event_id: int, user_group_id: int ) -> Optional["Enrollment"]: """Returns a specific enrollment (or none) that match the user group id and event id""" - return Enrollment.query.filter( - Enrollment.event_id == event_id, Enrollment.usergroup_id == user_group_id - ).one_or_none() + return ( + db.session.execute( + select(Enrollment).filter( + Enrollment.event_id == event_id, + Enrollment.usergroup_id == user_group_id, + ) + ) + .scalars() + .one_or_none() + ) class EventTagAttachment(db.Model): @@ -123,7 +130,11 @@ def get_or_create(tags: Iterable[str]) -> list["EventTag"]: """ result = [] # noinspection PyUnresolvedReferences - existing_tags = EventTag.query.filter(EventTag.tag.in_(tags)).all() + existing_tags = ( + db.session.execute(select(EventTag).filter(EventTag.tag.in_(tags))) + .scalars() + .all() + ) existing_tags_dict = {tag.tag: tag for tag in existing_tags} for tag in tags: if tag in existing_tags_dict: @@ -242,23 +253,28 @@ def enrollments_count(self) -> EnrollmentCounts: """Returns the number of enrollments in the event""" # noinspection PyUnresolvedReferences has_extras = ( - EventGroup.query.filter( - (EventGroup.event_id == self.event_id) & EventGroup.extra.is_(True) + db.session.execute( + select(EventGroup.extra) + .filter( + (EventGroup.event_id == self.event_id) & EventGroup.extra.is_(True) + ) + .limit(1) ) - .with_entities(EventGroup.extra) + .scalars() .first() is not None ) - q = ( - Enrollment.query.filter(Enrollment.event_id == self.event_id) + stmt = ( + select(Enrollment) + .filter(Enrollment.event_id == self.event_id) .group_by(Enrollment.extra) - .with_entities( + .with_only_columns( Enrollment.extra, func.count(Enrollment.event_id).label("enrollments_count"), ) ) - res = {is_extra: count for is_extra, count in q} + res = {is_extra: count for is_extra, count in db.session.execute(stmt)} return EnrollmentCounts( res.get(False, 0), res.get(True, 0 if has_extras else None) ) @@ -274,9 +290,16 @@ def get_enrollment_right(self, user: User) -> EnrollmentRight: ug_ids = [ug.id for ug in user.groups] # noinspection PyUnresolvedReferences - event_groups = EventGroup.query.filter( - (EventGroup.event_id == self.event_id) & EventGroup.usergroup_id.in_(ug_ids) - ).all() + event_groups = ( + db.session.execute( + select(EventGroup).filter( + (EventGroup.event_id == self.event_id) + & EventGroup.usergroup_id.in_(ug_ids) + ) + ) + .scalars() + .all() + ) can_view_event_doc = False if self.origin_doc_id: can_view_event_doc = verify_view_access( @@ -298,7 +321,11 @@ def get_enrollment_right(self, user: User) -> EnrollmentRight: @staticmethod def get_by_id(event_id: int) -> Optional["Event"]: - return Event.query.filter_by(event_id=event_id).one_or_none() + return ( + db.session.execute(select(Event).filter_by(event_id=event_id)) + .scalars() + .one_or_none() + ) def to_json( self, @@ -334,12 +361,14 @@ def to_json( user_group_ids = [ug.id for ug in for_user.groups] # noinspection PyUnresolvedReferences e = ( - db.session.query(EventGroup.extra) - .filter( - (EventGroup.event_id == self.event_id) - & (EventGroup.usergroup_id.in_(user_group_ids)) - & (EventGroup.extra.is_(True)) + db.session.execute( + select(EventGroup.extra).filter( + (EventGroup.event_id == self.event_id) + & (EventGroup.usergroup_id.in_(user_group_ids)) + & (EventGroup.extra.is_(True)) + ) ) + .scalars() .first() ) meta |= { diff --git a/timApp/plugin/group_join/group_join.py b/timApp/plugin/group_join/group_join.py index 7ae2c536a9..0bf956c612 100644 --- a/timApp/plugin/group_join/group_join.py +++ b/timApp/plugin/group_join/group_join.py @@ -3,6 +3,7 @@ from flask import render_template_string, Response from marshmallow import missing +from sqlalchemy import select from timApp.auth.accesshelper import verify_logged_in from timApp.auth.sessioninfo import get_current_user_object @@ -136,11 +137,18 @@ def _do_group_op( apply: Callable[[User, UserGroup], None], ) -> tuple[bool, bool, dict[str, str]]: user_groups: set[str] = set( - g for g, in user.get_groups(include_expired=False).with_entities(UserGroup.name) + g + for g, in db.session.execute( + user.get_groups(include_expired=False).with_only_columns(UserGroup.name) + ) ) result = dict.fromkeys(groups, "") - ugs: list[UserGroup] = UserGroup.query.filter(UserGroup.name.in_(groups)).all() + ugs: list[UserGroup] = ( + db.session.execute(select(UserGroup).filter(UserGroup.name.in_(groups))) + .scalars() + .all() + ) all_ok = True is_course = False diff --git a/timApp/plugin/importdata/importData.py b/timApp/plugin/importdata/importData.py index e0de0dea46..309bc3ca3a 100644 --- a/timApp/plugin/importdata/importData.py +++ b/timApp/plugin/importdata/importData.py @@ -10,9 +10,11 @@ import requests from flask import render_template_string from marshmallow.utils import missing +from sqlalchemy import select from timApp.plugin.jsrunner.jsrunner import jsrunner_run, JsRunnerParams, JsRunnerError from timApp.tim_app import csrf +from timApp.timdb.sqa import db from timApp.user.hakaorganization import HakaOrganization from timApp.user.personaluniquecode import PersonalUniqueCode, SchacPersonalUniqueCode from timApp.user.user import User, UserInfo @@ -365,26 +367,27 @@ def answer(args: ImportDataAnswerModel) -> PluginAnswerResp: org = None if m: org = m.group("org") - q = ( - User.query.join(PersonalUniqueCode) + stmt = ( + select(User) + .join(PersonalUniqueCode) .filter(PersonalUniqueCode.code.in_(idents)) .join(HakaOrganization) .filter_by(name=org) - .with_entities(PersonalUniqueCode.code, User) + .with_only_columns(PersonalUniqueCode.code, User) ) - users = {c: u for c, u in q.all()} + users = {c: u for c, u in db.session.execute(stmt)} elif id_prop == "username": - q = User.query.filter(User.name.in_(idents)) - users = {u.name: u for u in q.all()} + stmt = select(User).filter(User.name.in_(idents)) + users = {u.name: u for u in db.session.execute(stmt).scalars()} elif id_prop == "id": try: - q = User.query.filter(User.id.in_([int(i) for i in idents])) + stmt = select(User).filter(User.id.in_([int(i) for i in idents])) except ValueError as e: return args.make_answer_error(f"User ids must be ints ({e})") - users = {str(u.id): u for u in q.all()} + users = {str(u.id): u for u in db.sesion.execute(stmt).scalars()} elif id_prop == "email": - q = User.query.filter(User.email.in_(idents)) - users = {u.email: u for u in q.all()} + stmt = select(User).filter(User.email.in_(idents)) + users = {u.email: u for u in db.session.execute(stmt).scalars()} else: return args.make_answer_error( f"Invalid joinProperty: {args.markup.joinProperty}" diff --git a/timApp/plugin/jsrunner/util.py b/timApp/plugin/jsrunner/util.py index fe232bd577..76dedd0253 100644 --- a/timApp/plugin/jsrunner/util.py +++ b/timApp/plugin/jsrunner/util.py @@ -5,7 +5,7 @@ from datetime import datetime from typing import TypedDict, Any, DefaultDict, Literal -from sqlalchemy import func +from sqlalchemy import func, select from timApp.answer.answer import Answer from timApp.answer.answers import get_global_answers @@ -78,7 +78,11 @@ def handle_jsrunner_groups(groupdata: JsrunnerGroups | None, curr_user: User) -> group_members_state[ug] = UserGroupMembersState( before=current_state, after=set(current_state) ) - users: list[User] = User.query.filter(User.id.in_(uids)).all() + users: list[User] = ( + db.session.execute(select(User).filter(User.id.in_(uids))) + .scalars() + .all() + ) found_user_ids = {u.id for u in users} missing_ids = set(uids) - found_user_ids if missing_ids: @@ -224,7 +228,9 @@ def save_fields( doc_map: dict[int, DocInfo] = {} user_map: dict[int, User] = { u.id: u - for u in User.query.filter(User.id.in_(x["user"] for x in save_obj)).all() + for u in db.session.execute( + select(User).filter(User.id.in_(x["user"] for x in save_obj)) + ).scalars() } # We need this separate "add_users_to_group" parameter because the plugin may have reported missing users. @@ -336,7 +342,8 @@ def save_fields( for key in user["fields"].keys() } sq = ( - Answer.query.filter( + select(Answer) + .filter( Answer.task_id.in_( [tid.doc_task for tid in parsed_task_ids.values() if not tid.is_global] ) @@ -345,14 +352,14 @@ def save_fields( .join(User, Answer.users) .filter(User.id.in_(user_map.keys())) .group_by(User.id, Answer.task_id) - .with_entities(func.max(Answer.id).label("aid"), User.id.label("uid")) + .with_only_columns(func.max(Answer.id).label("aid"), User.id.label("uid")) .subquery() ) - datas: list[tuple[int, Answer]] = ( - Answer.query.join(sq, Answer.id == sq.c.aid) - .with_entities(sq.c.uid, Answer) - .all() - ) + datas: list[tuple[int, Answer]] = db.session.execute( + select(Answer) + .join(sq, Answer.id == sq.c.aid) + .with_only_columns(sq.c.uid, Answer) + ).all() global_answers = get_global_answers(parsed_task_ids) answer_map: defaultdict[int, dict[str, Answer]] = defaultdict(dict) for uid, a in datas: diff --git a/timApp/plugin/plugin.py b/timApp/plugin/plugin.py index 246b92ac0a..51c790bf87 100644 --- a/timApp/plugin/plugin.py +++ b/timApp/plugin/plugin.py @@ -8,6 +8,7 @@ import yaml from marshmallow import missing, ValidationError, EXCLUDE +from sqlalchemy import select from timApp.answer.answer import Answer from timApp.auth.accesstype import AccessType @@ -32,6 +33,7 @@ from timApp.plugin.taskid import TaskId, UnvalidatedTaskId, TaskIdAccess from timApp.printing.printsettings import PrintFormat from timApp.timdb.exceptions import TimDbException +from timApp.timdb.sqa import db from timApp.user.user import User from timApp.util.rndutils import myhash, SeedClass from timApp.util.utils import try_load_json, get_current_time, Range @@ -600,11 +602,17 @@ def set_access_end_for_user(self, user: User | None = None): b = TaskBlock.get_by_task(self.task_id.doc_task) if not b: return - ba = BlockAccess.query.filter_by( - block_id=b.id, - type=AccessType.view.value, - usergroup_id=current_user.get_personal_group().id, - ).first() + ba = ( + db.session.execute( + select(BlockAccess).filter_by( + block_id=b.id, + type=AccessType.view.value, + usergroup_id=current_user.get_personal_group().id, + ) + ) + .scalars() + .first() + ) if not ba: return self.access_end_for_user = ba.accessible_to @@ -631,11 +639,17 @@ def hidden_by_prerequisite(self) -> bool: self.hidden = True b = TaskBlock.get_by_task(tid.doc_task) if b: - ba = BlockAccess.query.filter_by( - block_id=b.id, - type=AccessType.view.value, - usergroup_id=current_user.get_personal_group().id, - ).first() + ba = ( + db.session.execute( + select(BlockAccess).filter_by( + block_id=b.id, + type=AccessType.view.value, + usergroup_id=current_user.get_personal_group().id, + ) + ) + .scalars() + .first() + ) if ba and ba.accessible_to: if ba.accessible_to < get_current_time(): self.hidden = False diff --git a/timApp/plugin/pluginControl.py b/timApp/plugin/pluginControl.py index 9da5aa7692..38c24f6728 100644 --- a/timApp/plugin/pluginControl.py +++ b/timApp/plugin/pluginControl.py @@ -377,6 +377,7 @@ def check_task_access(errs: ErrorMap, p_range: Range, plugin_name: str, tid: Tas def get_answers(user, task_ids, answer_map): + # FIXME: SQLAlchemy dynamic col = func.max(Answer.id).label("col") cnt = func.count(Answer.id).label("cnt") if user is None: @@ -701,7 +702,7 @@ def pluginify( ) continue if not isinstance(plugin_htmls, list): - for ((idx, r), plugin) in plugin_block_map.items(): + for (idx, r), plugin in plugin_block_map.items(): plugin.plugin_lazy = plugin_lazy placements[idx].set_error( r, diff --git a/timApp/plugin/plugintype.py b/timApp/plugin/plugintype.py index f23e1b29b4..d8315281b5 100644 --- a/timApp/plugin/plugintype.py +++ b/timApp/plugin/plugintype.py @@ -2,6 +2,7 @@ from typing import Any import filelock +from sqlalchemy import select from sqlalchemy.exc import IntegrityError import timApp @@ -40,7 +41,11 @@ class PluginType(db.Model, PluginTypeBase): @staticmethod def resolve(p_type: str) -> "PluginType": - pt = PluginType.query.filter_by(type=p_type).first() + pt = ( + db.session.execute(select(PluginType).filter_by(type=p_type)) + .scalars() + .first() + ) if pt: return pt @@ -62,7 +67,11 @@ def resolve(p_type: str) -> "PluginType": raise # We have to re-query the database since the other session was closed - return PluginType.query.filter_by(type=p_type).one() + return ( + db.session.execute(select(PluginType).filter_by(type=p_type)) + .scalars() + .one() + ) def get_type(self) -> str: return self.type diff --git a/timApp/plugin/tableform/tableForm.py b/timApp/plugin/tableform/tableForm.py index 52bba1f070..03233c700b 100644 --- a/timApp/plugin/tableform/tableForm.py +++ b/timApp/plugin/tableform/tableForm.py @@ -8,6 +8,7 @@ from flask import render_template_string, Response, send_file from marshmallow.utils import missing +from sqlalchemy import select from sqlalchemy.orm import joinedload from webargs.flaskparser import use_args @@ -30,6 +31,7 @@ from timApp.sisu.parse_display_name import parse_sisu_group_display_name from timApp.sisu.sisu import get_potential_groups from timApp.tim_app import csrf +from timApp.timdb.sqa import db from timApp.user.user import User, get_membership_end, get_membership_added from timApp.user.usergroup import UserGroup from timApp.util.flask.requesthelper import ( @@ -177,19 +179,20 @@ def get_sisu_group_desc_for_table(g: UserGroup) -> str: def get_sisugroups(user: User, sisu_id: str | None) -> "TableFormObj": gs = get_potential_groups(user, sisu_id) - docs_with_course_tag = ( - Tag.query.filter_by(type=TagType.CourseCode) - .with_entities(Tag.block_id) - .subquery() - ) + docs_with_course_tag = select(Tag.block_id).filter_by(type=TagType.CourseCode) tags = ( - Tag.query.filter( - Tag.name.in_([GROUP_TAG_PREFIX + g.name for g in gs]) - & Tag.block_id.in_(docs_with_course_tag) + db.session.execute( + select(Tag) + .filter( + Tag.name.in_([GROUP_TAG_PREFIX + g.name for g in gs]) + & Tag.block_id.in_(docs_with_course_tag) + ) + .options(joinedload(Tag.block).joinedload(Block.docentries)) ) - .options(joinedload(Tag.block).joinedload(Block.docentries)) + .scalars() .all() ) + tag_map = {t.name[len(GROUP_TAG_PREFIX) :]: t for t in tags} def get_course_page(ug: UserGroup) -> str | None: diff --git a/timApp/plugin/timtable/timTable.py b/timApp/plugin/timtable/timTable.py index a955d0b66f..96abdaa70a 100644 --- a/timApp/plugin/timtable/timTable.py +++ b/timApp/plugin/timtable/timTable.py @@ -990,8 +990,8 @@ def tim_table_add_multi_cell_value(cells_to_save, d, plug, multi, must_call_dumb # verify_edit_access(d) if is_in_global_append_mode(plug): raise NotImplementedError - user = get_current_user_object() - q = RowOwnerInfo.query + # user = get_current_user_object() + # q = RowOwnerInfo.query # TODO figure out filter # q.filter() else: @@ -1038,8 +1038,8 @@ def tim_table_save_cell_value(cell_content, docid, parid, row, col): # verify_edit_access(d) if is_in_global_append_mode(plug): raise NotImplementedError - user = get_current_user_object() - q = RowOwnerInfo.query + # user = get_current_user_object() + # q = RowOwnerInfo.query # TODO figure out filter # q.filter() else: diff --git a/timApp/plugin/userselect/action_queue.py b/timApp/plugin/userselect/action_queue.py index 03ea58727e..a82e273ca2 100644 --- a/timApp/plugin/userselect/action_queue.py +++ b/timApp/plugin/userselect/action_queue.py @@ -9,7 +9,7 @@ from typing import Literal import filelock -from sqlalchemy import tuple_ +from sqlalchemy import tuple_, select from timApp.answer.backup import sync_user_group_memberships_if_enabled from timApp.plugin.userselect.dist_right_util import ( @@ -293,20 +293,28 @@ def apply_pending_actions_impl() -> None: # noinspection PyUnresolvedReferences group_cache: dict[str, UserGroup] = { ug.name: ug - for ug in UserGroup.query.filter(UserGroup.name.in_(group_names)) + for ug in db.session.execute( + select(UserGroup).filter(UserGroup.name.in_(group_names)) + ).scalars() } # noinspection PyUnresolvedReferences user_cache: dict[str, User] = { - u.name: u for u in User.query.filter(User.name.in_(user_names)) + u.name: u + for u in db.session.execute( + select(User).filter(User.name.in_(user_names)) + ).scalars() } memberships_cache: dict[tuple[int, int], UserGroupMember] = { (m.usergroup_id, m.user_id): m - for m in UserGroupMember.query.join(UserGroup, UserGroupMember.group) - .join(User, UserGroupMember.user) - .filter( - tuple_(UserGroup.name, User.name).in_(memberships_to_expire) - & membership_current - ) + for m in db.session.execute( + select(UserGroupMember) + .join(UserGroup, UserGroupMember.group) + .join(User, UserGroupMember.user) + .filter( + tuple_(UserGroup.name, User.name).in_(memberships_to_expire) + & membership_current + ) + ).scalars() } for a in effective_group_actions: diff --git a/timApp/plugin/userselect/userselect.py b/timApp/plugin/userselect/userselect.py index 216e9bb143..bdb80b4a13 100644 --- a/timApp/plugin/userselect/userselect.py +++ b/timApp/plugin/userselect/userselect.py @@ -4,6 +4,7 @@ import filelock from flask import render_template_string, Response, current_app +from sqlalchemy import select from timApp.answer.backup import sync_user_group_memberships_if_enabled from timApp.auth.accesshelper import verify_logged_in, verify_view_access, verify_admin @@ -516,13 +517,23 @@ def undo_field_actions( def get_groups( cur_user: User, add: list[str], remove: list[str], change_all_groups: list[str] ) -> tuple[list[UserGroup], list[UserGroup], list[UserGroup]]: - add_groups: list[UserGroup] = UserGroup.query.filter(UserGroup.name.in_(add)).all() - remove_groups: list[UserGroup] = UserGroup.query.filter( - UserGroup.name.in_(remove) - ).all() - change_all_groups_ugs: list[UserGroup] = UserGroup.query.filter( - UserGroup.name.in_(change_all_groups) - ).all() + add_groups: list[UserGroup] = ( + db.session.execute(select(UserGroup).filter(UserGroup.name.in_(add))) + .scalars() + .all() + ) + remove_groups: list[UserGroup] = ( + db.session.execute(select(UserGroup).filter(UserGroup.name.in_(remove))) + .scalars() + .all() + ) + change_all_groups_ugs: list[UserGroup] = ( + db.session.execute( + select(UserGroup).filter(UserGroup.name.in_(change_all_groups)) + ) + .scalars() + .all() + ) all_groups: dict[str, UserGroup] = { ug.name: ug for ug in (add_groups + remove_groups + change_all_groups_ugs) } @@ -718,11 +729,19 @@ def apply_permission_actions( for to_confirm in confirm: doc_entry = doc_entries[to_confirm.doc_path] - ba_confirm: BlockAccess | None = BlockAccess.query.filter_by( - type=to_confirm.type.value, - block_id=doc_entry.block.id, - usergroup_id=user_group.id, - ).first() + ba_confirm: BlockAccess | None = ( + db.session.execute( + select(BlockAccess) + .filter_by( + type=to_confirm.type.value, + block_id=doc_entry.block.id, + usergroup_id=user_group.id, + ) + .limit(1) + ) + .scalars() + .first() + ) if ba_confirm and ba_confirm.require_confirm: ba_confirm.do_confirm() update_messages.append( @@ -731,11 +750,19 @@ def apply_permission_actions( for to_change in change_time: doc_entry = doc_entries[to_change.doc_path] - ba_change: BlockAccess | None = BlockAccess.query.filter_by( - type=to_change.type.value, - block_id=doc_entry.block.id, - usergroup_id=user_group.id, - ).first() + ba_change: BlockAccess | None = ( + db.session.execute( + select(BlockAccess) + .filter_by( + type=to_change.type.value, + block_id=doc_entry.block.id, + usergroup_id=user_group.id, + ) + .limit(1) + ) + .scalars() + .first() + ) if ba_change and ba_change.accessible_to is not None: ba_change.accessible_to += timedelta(minutes=to_change.minutes) update_messages.append( diff --git a/timApp/printing/documentprinter.py b/timApp/printing/documentprinter.py index c02f9baf48..c7d19f70bf 100644 --- a/timApp/printing/documentprinter.py +++ b/timApp/printing/documentprinter.py @@ -269,7 +269,6 @@ def get_content( ] ] = [] for par in pars: - # do not print document settings pars if par.is_setting(): continue @@ -577,7 +576,6 @@ def write_to_format( """ with tempfile.NamedTemporaryFile(suffix=".latex", delete=True) as template_file: - if self._template_to_use: template_content = DocumentPrinter.parse_template_content( doc_to_print=self._doc_entry, template_doc=self._template_to_use @@ -651,7 +649,6 @@ def write_to_format( # TODO: add also variables from texpandocvariables document setting, but this may lead to security hole? try: - tim_convert_text( source=src, from_format=from_format, @@ -741,7 +738,6 @@ def get_all_templates(doc_entry: DocEntry, current_user: User) -> list[DocInfo]: current_folder = doc_entry.parent while current_folder is not None: - path = os.path.join(current_folder.get_full_path(), TEMPLATES_FOLDER) templates_folder = Folder.find_by_path(path) @@ -868,15 +864,20 @@ def get_printed_document_path_from_db( ) -> str | None: # noinspection PyUnresolvedReferences existing_print: PrintedDoc | None = ( - PrintedDoc.query.filter_by( - doc_id=self._doc_entry.id, - template_doc_id=self.get_template_id(), - file_type=file_type.value, - version=self.hash_doc_print( - plugins_user_print=plugins_user_print, url_macros=url_macros - ), + db.session.execute( + select(PrintedDoc) + .filter_by( + doc_id=self._doc_entry.id, + template_doc_id=self.get_template_id(), + file_type=file_type.value, + version=self.hash_doc_print( + plugins_user_print=plugins_user_print, url_macros=url_macros + ), + ) + .order_by(PrintedDoc.id.desc()) + .limit(1) ) - .order_by(PrintedDoc.id.desc()) + .scalars() .first() ) if existing_print is None or not os.path.exists(existing_print.path_to_file): @@ -999,7 +1000,6 @@ def tim_convert_input( else: f.write(source) else: - input_file = [source] if not string_input else [] args = [pandoc_path, "--from=" + from_format, "--to=" + to] diff --git a/timApp/readmark/readings.py b/timApp/readmark/readings.py index 93910bcd95..04124f81af 100644 --- a/timApp/readmark/readings.py +++ b/timApp/readmark/readings.py @@ -2,7 +2,8 @@ from datetime import timedelta from typing import DefaultDict -from sqlalchemy.orm import Query +from sqlalchemy import select, func, delete +from sqlalchemy.sql import Select from timApp.document.docparagraph import DocParagraph from timApp.document.document import Document @@ -28,32 +29,32 @@ def has_anything_read(usergroup_ids: list[int], doc: Document) -> bool: # Custom query for speed ids = doc.get_referenced_document_ids() ids.add(doc.doc_id) - query = db.session.query(ReadParagraph.id).filter( + query = select(ReadParagraph.id).filter( ReadParagraph.doc_id.in_(ids) & (ReadParagraph.usergroup_id.in_(usergroup_ids)) & (ReadParagraph.type == ReadParagraphType.click_red) ) # Normal query is generally faster than an "exists" subquery even if it causes extra data to be loaded - return query.first() is not None + return db.session.execute(query).scalars().first() is not None def get_readings_filtered_query( usergroup_id: int, doc: Document, filter_condition=None -) -> Query: - q = get_readings_query(usergroup_id, doc) +) -> Select: + stmt = get_readings_query(usergroup_id, doc) if filter_condition is not None: - q = q.filter(filter_condition) - return q + stmt = stmt.filter(filter_condition) + return stmt -def get_clicked_readings_query(doc: Document) -> Query: - return ReadParagraph.query.filter( +def get_clicked_readings_query(doc: Document) -> Select: + return select(ReadParagraph).filter( (ReadParagraph.doc_id == doc.doc_id) & (ReadParagraph.type == ReadParagraphType.click_red) ) -def get_readings_query(usergroup_id: int, doc: Document) -> Query: +def get_readings_query(usergroup_id: int, doc: Document) -> Select: """Gets the reading info for a document for a user. :param doc: The document for which to get the readings. @@ -62,9 +63,13 @@ def get_readings_query(usergroup_id: int, doc: Document) -> Query: """ ids = doc.get_referenced_document_ids() ids.add(doc.doc_id) - return ReadParagraph.query.filter( - ReadParagraph.doc_id.in_(ids) & (ReadParagraph.usergroup_id == usergroup_id) - ).order_by(ReadParagraph.timestamp) + return ( + select(ReadParagraph) + .filter( + ReadParagraph.doc_id.in_(ids) & (ReadParagraph.usergroup_id == usergroup_id) + ) + .order_by(ReadParagraph.timestamp) + ) def mark_read( @@ -86,9 +91,11 @@ def mark_read( def mark_all_read(usergroup_id: int, doc: Document): existing = { (r.par_id, r.doc_id): r - for r in get_readings_query(usergroup_id, doc).filter( - ReadParagraph.type == ReadParagraphType.click_red - ) + for r in db.session.execute( + get_readings_query(usergroup_id, doc).filter( + ReadParagraph.type == ReadParagraphType.click_red + ) + ).scalars() } for par in doc: e = existing.get((par.get_id(), doc.doc_id)) @@ -102,11 +109,23 @@ def remove_all_read_marks(doc: Document): # usually you'd use get_referenced_document_ids to get all document IDs # Since we're deleting read marks here, it's better to be safe and only remove marks only # for paragraphs defined directly in the document - get_clicked_readings_query(doc).delete(synchronize_session=False) + db.session.execute( + delete(ReadParagraph) + .where( + ReadParagraph.id.in_( + get_clicked_readings_query(doc).with_only_columns(ReadParagraph.id) + ) + ) + .execution_options(synchronize_session=False) + ) def get_read_usergroups_count(doc: Document): - return get_clicked_readings_query(doc).distinct(ReadParagraph.usergroup_id).count() + return db.session.scalar( + get_clicked_readings_query(doc) + .distinct(ReadParagraph.usergroup_id) + .with_only_columns(func.count()) + ) def copy_readings(src_par: DocParagraph, dest_par: DocParagraph): @@ -116,18 +135,22 @@ def copy_readings(src_par: DocParagraph, dest_par: DocParagraph): ): return - src_par_query = ReadParagraph.query.filter_by( + src_par_stmt = select(ReadParagraph).filter_by( doc_id=src_par.doc.doc_id, par_id=src_par.get_id() ) - ReadParagraph.query.filter( - (ReadParagraph.doc_id == dest_par.doc.doc_id) - & (ReadParagraph.par_id == dest_par.get_id()) - & ReadParagraph.usergroup_id.in_( - src_par_query.with_entities(ReadParagraph.usergroup_id) + db.session.execute( + delete(ReadParagraph) + .where( + (ReadParagraph.doc_id == dest_par.doc.doc_id) + & (ReadParagraph.par_id == dest_par.get_id()) + & ReadParagraph.usergroup_id.in_( + src_par_stmt.with_only_columns(ReadParagraph.usergroup_id) + ) ) - ).delete(synchronize_session="fetch") + .execution_options(synchronize_session="fetch") + ) - for p in src_par_query.all(): # type: ReadParagraph + for p in db.session.execute(src_par_stmt).scalars(): # type: ReadParagraph db.session.add( ReadParagraph( usergroup_id=p.usergroup_id, diff --git a/timApp/readmark/routes.py b/timApp/readmark/routes.py index 5b2dc2bcf1..315c0c2b97 100644 --- a/timApp/readmark/routes.py +++ b/timApp/readmark/routes.py @@ -1,9 +1,8 @@ from dataclasses import dataclass -from typing import Optional from flask import Blueprint, request from flask import current_app, Response -from sqlalchemy import func, distinct, true +from sqlalchemy import func, distinct, true, select from sqlalchemy.exc import IntegrityError from timApp.auth.accesshelper import ( @@ -131,13 +130,18 @@ def set_read_paragraph(doc_id, par_id, read_type=None, unread=False): for p in pars: if unread: rp = ( - ReadParagraph.query.filter_by( - usergroup_id=group_id, - doc_id=p.get_doc_id(), - par_id=p.get_id(), - type=paragraph_type, + db.session.execute( + select(ReadParagraph) + .filter_by( + usergroup_id=group_id, + doc_id=p.get_doc_id(), + par_id=p.get_id(), + type=paragraph_type, + ) + .order_by(ReadParagraph.timestamp.desc()) + .limit(1) ) - .order_by(ReadParagraph.timestamp.desc()) + .scalars() .first() ) if not rp: @@ -210,15 +214,16 @@ def get_statistics(doc_path): if group_opt: group_names = split_by_semicolon(group_opt) extra_condition = extra_condition & UserGroup.name.in_( - User.query.join(UserGroup, User.groups) + select(User.name) + .join(UserGroup, User.groups) .filter(UserGroup.name.in_(group_names)) - .with_entities(User.name) ) if consent: extra_condition = extra_condition & UserGroup.id.in_( - User.query.join(UserGroup, User.groups) + select(UserGroup.id) + .select_from(User) + .join(UserGroup, User.groups) .filter(User.consent == consent) - .with_entities(UserGroup.id) ) if block_opt: block_ids = split_by_semicolon(block_opt) @@ -251,14 +256,14 @@ def get_statistics(doc_path): raise RouteException( f"Invalid sort option. Possible values are {seq_to_str(column_names)}." ) - q = ( - UserGroup.query.join(ReadParagraph) + stmt = ( + select(UserGroup) + .join(ReadParagraph) .filter_by(doc_id=d.id) .filter(extra_condition) - .add_columns(*cols) .group_by(UserGroup) .order_by(col_to_sort) - .with_entities(UserGroup.name, *cols) + .with_only_columns(UserGroup.name, *cols) ) def maybe_hide_name_from_row(row): @@ -271,12 +276,12 @@ def row_to_dict(row): return dict(zip(column_names, maybe_hide_name_from_row(row))) if result_format == "count": - reads = list(map(row_to_dict, q.all())) + reads = list(map(row_to_dict, db.session.execute(stmt).all())) return Response(str(len(reads)), mimetype="text/plain") if result_format == "userid": - reads = list(map(row_to_dict, q.all())) + reads = list(map(row_to_dict, db.session.execute(stmt).all())) result = "" for r in reads: @@ -289,8 +294,8 @@ def row_to_dict(row): def gen_rows(): yield column_names - yield from (maybe_hide_name_from_row(row) for row in q) + yield from (maybe_hide_name_from_row(row) for row in stmt) return csv_response(gen_rows(), dialect=csv_dialect) else: - return json_response(list(map(row_to_dict, q.all()))) + return json_response(list(map(row_to_dict, stmt.all()))) diff --git a/timApp/scheduling/scheduling_routes.py b/timApp/scheduling/scheduling_routes.py index 7e6dd13409..b4c670af62 100644 --- a/timApp/scheduling/scheduling_routes.py +++ b/timApp/scheduling/scheduling_routes.py @@ -4,6 +4,7 @@ from flask import current_app, Response from isodate import Duration +from sqlalchemy import select from sqlalchemy.orm import joinedload from timApp.auth.accesshelper import ( @@ -60,12 +61,13 @@ def get_scheduled_functions(all_users: bool = False) -> Response: verify_logged_in() u = get_current_user_object() - q = ( - Block.query.filter_by(type_id=BlockType.ScheduledFunction.value) + stmt = ( + select(PeriodicTask) + .select_from(Block) + .filter_by(type_id=BlockType.ScheduledFunction.value) .join(BlockAccess) .filter(BlockAccess.type == AccessType.owner.value) .join(PeriodicTask) - .with_entities(PeriodicTask) .options( joinedload(PeriodicTask.block) .joinedload(Block.accesses) @@ -77,13 +79,21 @@ def get_scheduled_functions(all_users: bool = False) -> Response: verify_admin() if not all_users: - q = q.filter(BlockAccess.block_id.in_(get_owned_objects_query(u).subquery())) + stmt = stmt.filter( + BlockAccess.block_id.in_(get_owned_objects_query(u).subquery()) + ) - scheduled_fns: list[PeriodicTask] = q.all() + scheduled_fns: list[PeriodicTask] = db.session.execute(stmt).scalars().all() - docentries = DocEntry.query.filter( - DocEntry.id.in_([t.task_id.doc_id for t in scheduled_fns]) - ).all() + docentries = ( + db.session.execute( + select(DocEntry).filter( + DocEntry.id.in_([t.task_id.doc_id for t in scheduled_fns]) + ) + ) + .scalars() + .all() + ) d_map = {d.id: d for d in docentries} def gen() -> Generator[ScheduledFunctionItem, None, None]: @@ -131,16 +141,26 @@ def add_scheduled_function( min_interval = current_app.config["MINIMUM_SCHEDULED_FUNCTION_INTERVAL"] if secs < min_interval: raise RouteException(f"Minimum interval is {min_interval} seconds.") - schedule = IntervalSchedule.query.filter_by( - every=secs, period=IntervalSchedule.SECONDS - ).first() + schedule: IntervalSchedule | None = ( + db.session.execute( + select(IntervalSchedule) + .filter_by(every=secs, period=IntervalSchedule.SECONDS) + .limit(1) + ) + .scalars() + .first() + ) if not schedule: schedule = IntervalSchedule(every=secs, period=IntervalSchedule.SECONDS) db.session.add(schedule) assert p.task_id is not None task_id_str = p.task_id.doc_task - existing = PeriodicTask.query.filter_by(name=task_id_str).first() + existing = ( + db.session.execute(select(PeriodicTask).filter_by(name=task_id_str).limit(1)) + .scalars() + .first() + ) if existing: raise RouteException( "A scheduled function for this plugin already exists. Remove the existing function first before adding a " @@ -166,10 +186,18 @@ def delete_scheduled_plugin_run( function_id: int, ) -> Response: pto: PeriodicTask = ( - Block.query.filter_by(id=function_id, type_id=BlockType.ScheduledFunction.value) - .join(PeriodicTask) - .with_entities(PeriodicTask) - ).first() + ( + db.session.execute( + select(PeriodicTask) + .select_from(Block) + .filter_by(id=function_id, type_id=BlockType.ScheduledFunction.value) + .join(PeriodicTask) + .limit(1) + ) + ) + .scalars() + .first() + ) if not pto: raise NotExist("scheduled function not found") u = get_current_user_object() diff --git a/timApp/sisu/scim.py b/timApp/sisu/scim.py index 1ffc24f76f..38640e6f52 100644 --- a/timApp/sisu/scim.py +++ b/timApp/sisu/scim.py @@ -2,9 +2,10 @@ import traceback from dataclasses import field, dataclass from functools import cached_property -from typing import Optional, Any, Generator +from typing import Any, Generator from flask import Blueprint, request, current_app, Response +from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import aliased from webargs.flaskparser import use_args @@ -218,11 +219,13 @@ def get_groups(args: GetGroupsModel) -> Response: if not m: raise SCIMException(422, "Unsupported filter") groups = ( - ScimUserGroup.query.filter( - ScimUserGroup.external_id.startswith(scim_group_to_tim(m.group(1))) + db.session.execute( + select(UserGroup) + .select_from(ScimUserGroup) + .filter(ScimUserGroup.external_id.startswith(scim_group_to_tim(m.group(1)))) + .join(UserGroup) ) - .join(UserGroup) - .with_entities(UserGroup) + .scalars() .all() ) @@ -387,9 +390,15 @@ def update_users(ug: UserGroup, args: SCIMGroupModel) -> None: added_users = set() scimuser = User.get_scimuser() - existing_accounts: list[User] = User.query.filter( - User.name.in_(current_usernames) | User.email.in_(emails) - ).all() + existing_accounts: list[User] = ( + db.session.execute( + select(User).filter( + User.name.in_(current_usernames) | User.email.in_(emails) + ) + ) + .scalars() + .all() + ) existing_accounts_dict: dict[str, User] = {u.name: u for u in existing_accounts} existing_accounts_by_email_dict: dict[str, User] = { u.email: u for u in existing_accounts @@ -577,9 +586,14 @@ def members() -> Generator[dict, None, None]: def try_get_group_by_scim(group_id: str) -> UserGroup | None: try: ug = ( - ScimUserGroup.query.filter_by(external_id=scim_group_to_tim(group_id)) - .join(UserGroup) - .with_entities(UserGroup) + db.session.execute( + select(UserGroup) + .select_from(ScimUserGroup) + .filter_by(external_id=scim_group_to_tim(group_id)) + .join(UserGroup) + .limit(1) + ) + .scalars() .first() ) except ValueError: diff --git a/timApp/sisu/sisu.py b/timApp/sisu/sisu.py index 5169758f66..84e92f03e4 100644 --- a/timApp/sisu/sisu.py +++ b/timApp/sisu/sisu.py @@ -8,7 +8,7 @@ from flask import Blueprint, current_app, request, Response from marshmallow import validates, ValidationError from marshmallow.utils import _Missing, missing -from sqlalchemy import any_, true +from sqlalchemy import any_, true, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload from webargs.flaskparser import use_args @@ -739,7 +739,11 @@ def get_sisu_assessments( usergroups = groups_setting else: usergroups = groups - ugs = UserGroup.query.filter(UserGroup.name.in_(usergroups)).all() + ugs = ( + db.session.execute(select(UserGroup).filter(UserGroup.name.in_(usergroups))) + .scalars() + .all() + ) requested = set(usergroups) found = {ug.name for ug in ugs} not_found_gs = requested - found diff --git a/timApp/slide/routes.py b/timApp/slide/routes.py index 0f36171224..db5fff0feb 100644 --- a/timApp/slide/routes.py +++ b/timApp/slide/routes.py @@ -1,5 +1,7 @@ import json +from sqlalchemy import select + from timApp.auth.accesshelper import get_doc_or_abort, verify_manage_access from timApp.slide.slidestatus import SlideStatus from timApp.timdb.sqa import db @@ -15,7 +17,11 @@ @slide_bp.get("/getslidestatus") def getslidestatus(doc_id: int): - status: SlideStatus = SlideStatus.query.filter_by(doc_id=doc_id).first() + status: SlideStatus = ( + db.session.execute(select(SlideStatus).filter_by(doc_id=doc_id).limit(1)) + .scalars() + .first() + ) st = status.status if status else None return json_response(json.loads(st)) diff --git a/timApp/tests/browser/test_model_answer.py b/timApp/tests/browser/test_model_answer.py index 4f0367da23..f26164e232 100644 --- a/timApp/tests/browser/test_model_answer.py +++ b/timApp/tests/browser/test_model_answer.py @@ -28,7 +28,7 @@ def test_generic_model_answer(self): ) self.test_user_2.grant_access(d, AccessType.view) db.session.commit() - db.session.refresh(Block.query.get(d.block.id)) + db.session.refresh(db.session.get(Block, d.block.id)) self.login_test2() self.login_browser_quick_test2() self.get(f"/getModelAnswer/{d.id}.lock", expect_status=403) @@ -118,7 +118,7 @@ def test_model_answer_previoustask(self): ) self.test_user_2.grant_access(d, AccessType.view) db.session.commit() - db.session.refresh(Block.query.get(d.block.id)) + db.session.refresh(db.session.get(Block, d.block.id)) self.login_test2() self.login_browser_quick_test2() self.goto_document(d) diff --git a/timApp/tests/browser/test_questions.py b/timApp/tests/browser/test_questions.py index 4d0a5ccde8..75a431c475 100644 --- a/timApp/tests/browser/test_questions.py +++ b/timApp/tests/browser/test_questions.py @@ -5,6 +5,7 @@ from selenium.webdriver.common.by import By from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support.select import Select +from sqlalchemy import select from timApp.answer.answer import Answer from timApp.document.yamlblock import YamlBlock @@ -13,6 +14,7 @@ find_button_by_text, find_by_attr_name, ) +from timApp.timdb.sqa import db ChoiceList = list[tuple[str, str]] ElementList = list[WebElement] @@ -386,6 +388,12 @@ def do_question_test( continue # check answer format is correct - a = Answer.query.filter_by(task_id=f'{d.id}.{qst_par.get_attr("taskId")}').one() + a = ( + db.session.execute( + select(Answer).filter_by(task_id=f'{d.id}.{qst_par.get_attr("taskId")}') + ) + .scalars() + .one() + ) self.assertEqual(expected_answer, a.content) self.assertEqual(expected_points, a.points) diff --git a/timApp/tests/browser/test_reviewcanvas.py b/timApp/tests/browser/test_reviewcanvas.py index 26d2c606fc..3e622ed2d9 100644 --- a/timApp/tests/browser/test_reviewcanvas.py +++ b/timApp/tests/browser/test_reviewcanvas.py @@ -48,7 +48,7 @@ def test_get_multiple_uploads_rights(self): ) self.test_user_2.grant_access(d2, AccessType.view) db.session.commit() - db.session.refresh(Block.query.get(d2.block.id)) + db.session.refresh(db.session.get(Block, d2.block.id)) self.login_test2() # Fail, upload is not testuser2's self.post_answer("reviewcanvas", f"{d2.id}.rc2", user_input, expect_status=403) @@ -65,7 +65,7 @@ def test_get_multiple_uploads_rights(self): self.get(pdfurl, expect_status=403) self.test_user_2.grant_access(d, AccessType.teacher) db.session.commit() - db.session.refresh(Block.query.get(d.block.id)) + db.session.refresh(db.session.get(Block, d.block.id)) self.get(pdfurl, expect_status=200) def test_corrupt_image(self): diff --git a/timApp/tests/browser/test_velps.py b/timApp/tests/browser/test_velps.py index a9095de40a..bbb7248d7c 100644 --- a/timApp/tests/browser/test_velps.py +++ b/timApp/tests/browser/test_velps.py @@ -129,7 +129,7 @@ def test_velp_user_filtering(self): db.session.commit() def_group_res = self.post(f"{d.id}/create_default_velp_group") default_group_id = def_group_res["id"] - vg = VelpGroup.query.get(default_group_id) + vg = db.session.get(VelpGroup, default_group_id) new_velp, _ = create_new_velp( self.test_user_1.id, "content", diff --git a/timApp/tests/db/test_personal_folder.py b/timApp/tests/db/test_personal_folder.py index 6162b2bb09..f08f031cb3 100644 --- a/timApp/tests/db/test_personal_folder.py +++ b/timApp/tests/db/test_personal_folder.py @@ -1,3 +1,5 @@ +from sqlalchemy import select + from timApp.folder.folder import Folder from timApp.tests.server.timroutetest import TimRouteTest from timApp.tim_app import app @@ -45,9 +47,17 @@ def test_anon_personal_folder(self): """Make sure personal folders aren't created for each anonymous request.""" self.logout() self.get("/") - folders = Folder.query.filter_by(location="users").all() + folders = ( + db.session.execute(select(Folder).filter_by(location="users")) + .scalars() + .all() + ) self.get("/") - folders_after = Folder.query.filter_by(location="users").all() + folders_after = ( + db.session.execute(select(Folder).filter_by(location="users")) + .scalars() + .all() + ) self.assertEqual(len(folders), len(folders_after)) def test_no_multiple_personal_folders(self): diff --git a/timApp/tests/db/test_plugin.py b/timApp/tests/db/test_plugin.py index 1884897127..e61f996a85 100644 --- a/timApp/tests/db/test_plugin.py +++ b/timApp/tests/db/test_plugin.py @@ -4,6 +4,7 @@ from timApp.document.viewcontext import default_view_ctx from timApp.plugin.plugin import Plugin from timApp.tests.db.timdbtest import TimDbTest +from timApp.timdb.sqa import db from timApp.util.flask.responsehelper import to_dict from timApp.util.utils import static_tim_doc @@ -29,7 +30,7 @@ def test_info(self): ) a_ids.append((a.id, a2.id)) for i, (aid, aid2) in enumerate(a_ids, start=1): - a: Answer = Answer.query.get(aid) + a: Answer = db.session.get(Answer, aid) self.assert_dict_subset( to_dict(a), { @@ -40,7 +41,7 @@ def test_info(self): }, ) self.assertEqual(i, a.get_answer_number()) - a = Answer.query.get(aid2) + a = db.session.get(Answer, aid2) self.assert_dict_subset( to_dict(a), { diff --git a/timApp/tests/db/test_users.py b/timApp/tests/db/test_users.py index e90f7b8d33..300dab5485 100644 --- a/timApp/tests/db/test_users.py +++ b/timApp/tests/db/test_users.py @@ -192,7 +192,7 @@ def test_timed_permissions(self): b = insert_block( BlockType.Document, "testing", [self.test_user_2.get_personal_group()] ) - user = User.query.get(TEST_USER_1_ID) + user = db.session.get(User, TEST_USER_1_ID) self.assertFalse(user.has_view_access(b)) v = AccessType.view @@ -220,7 +220,7 @@ def test_timed_permissions(self): self.remove(pg1, b, v) def test_last_name_switch(self): - for (fn1, fn2) in [ + for fn1, fn2 in [ (lambda x: x, last_name_to_first), (last_name_to_last, lambda x: x), ]: diff --git a/timApp/tests/server/test_autocounters.py b/timApp/tests/server/test_autocounters.py index 32ee7e712e..7d1465fbc4 100644 --- a/timApp/tests/server/test_autocounters.py +++ b/timApp/tests/server/test_autocounters.py @@ -8,6 +8,7 @@ from timApp.printing.documentprinter import DocumentPrinter from timApp.tests.db.timdbtest import TEST_USER_1_ID from timApp.tests.server.timroutetest import TimRouteTest +from timApp.timdb.sqa import db from timApp.user.user import User @@ -89,7 +90,8 @@ def doc_with_counters(self, docstr, htmls_ex, msg, setstr=None): printer = DocumentPrinter(d, template_to_use=None, urlroot="") counters = printer.get_autocounters( - UserContext.from_one_user(User.query.get(TEST_USER_1_ID)), default_view_ctx + UserContext.from_one_user(db.session.get(User, TEST_USER_1_ID)), + default_view_ctx, ) new_counter_macro_values = ( f'``` {{settings="counters"}}\n{counters.get_counter_macros()}```\n' diff --git a/timApp/tests/server/test_comments.py b/timApp/tests/server/test_comments.py index 32104ae5b1..fb43797195 100644 --- a/timApp/tests/server/test_comments.py +++ b/timApp/tests/server/test_comments.py @@ -1,6 +1,7 @@ from lxml import html from lxml.cssselect import CSSSelector from lxml.html import HtmlElement +from sqlalchemy import select from timApp.auth.accesstype import AccessType from timApp.document.docparagraph import DocParagraph @@ -301,7 +302,11 @@ def test_comment_in_referenced_area(self): area_a_middle = d.document.get_paragraphs()[2] orig_par = d2.document.get_paragraphs()[0] r = self.post_comment(area_a_middle, public=True, text="test", orig=orig_par) - note: UserNote = UserNote.query.order_by(UserNote.id.desc()).first() + note: UserNote = ( + db.session.execute(select(UserNote).order_by(UserNote.id.desc()).limit(1)) + .scalars() + .first() + ) self.assert_same_html( html.fromstring(r["texts"]), f""" @@ -340,7 +345,11 @@ def test_comment_in_referenced_area(self): # Posting a comment to an area boundary paragraph should return just the paragraph element and no stray area # start/end tags. r = self.post_comment(area_a_start, public=True, text="test", orig=orig_par) - note: UserNote = UserNote.query.order_by(UserNote.id.desc()).first() + note: UserNote = ( + db.session.execute(select(UserNote).order_by(UserNote.id.desc()).limit(1)) + .scalars() + .first() + ) self.assert_same_html( html.fromstring(r["texts"]), f""" diff --git a/timApp/tests/server/test_default_rights.py b/timApp/tests/server/test_default_rights.py index 100317c089..14e5735d05 100644 --- a/timApp/tests/server/test_default_rights.py +++ b/timApp/tests/server/test_default_rights.py @@ -1,6 +1,7 @@ from operator import itemgetter from dateutil import parser +from sqlalchemy import select from timApp.auth.accesstype import AccessType from timApp.auth.auth_models import BlockAccess @@ -20,7 +21,11 @@ class DefaultRightTest(TimRouteTest): def test_document_default_rights(self): self.login_test1() doc = self.create_doc().document - docentry = DocEntry.query.filter_by(id=doc.doc_id).one() + docentry = ( + db.session.execute(select(DocEntry).filter_by(id=doc.doc_id)) + .scalars() + .one() + ) folder: Folder = docentry.parent folder_owner_id = folder.owners[0].id kg = get_home_organization_group() diff --git a/timApp/tests/server/test_duration.py b/timApp/tests/server/test_duration.py index f86ad8945b..4f6c9533f4 100644 --- a/timApp/tests/server/test_duration.py +++ b/timApp/tests/server/test_duration.py @@ -1,5 +1,7 @@ from datetime import timedelta +from sqlalchemy import select + from timApp.auth.accesstype import AccessType from timApp.auth.auth_models import BlockAccess from timApp.document.docentry import DocEntry @@ -41,11 +43,17 @@ def test_duration_unlock(self): expect_contains=self.unlock_success, ) self.get(d.url_relative) - ba = BlockAccess.query.filter_by( - usergroup_id=self.get_test_user_2_group_id(), - block_id=doc_id, - type=get_access_type_id("view"), - ).one() + ba = ( + db.session.execute( + select(BlockAccess).filter_by( + usergroup_id=self.get_test_user_2_group_id(), + block_id=doc_id, + type=get_access_type_id("view"), + ) + ) + .scalars() + .one() + ) ba.accessible_to -= timedelta(days=2) db.session.commit() self.get( @@ -70,11 +78,17 @@ def test_duration_value_access_to_clamp(self): ) db.session.commit() self.get(d.url_relative, query_string={"unlock": "true"}) - ba = BlockAccess.query.filter_by( - usergroup_id=self.get_test_user_2_group_id(), - block_id=d.id, - type=AccessType.view.value, - ).one() + ba = ( + db.session.execute( + select(BlockAccess).filter_by( + usergroup_id=self.get_test_user_2_group_id(), + block_id=d.id, + type=AccessType.view.value, + ) + ) + .scalars() + .one() + ) real_duration = (ba.accessible_to - get_current_time()).total_seconds() delta_access_seconds = delta_access.total_seconds() self.assertAlmostEqual(real_duration, delta_access_seconds, delta=1) @@ -162,11 +176,17 @@ def test_timed_duration_unlock(self): expect_contains=self.unlock_success, ) self.get(d.url_relative) - ba = BlockAccess.query.filter_by( - usergroup_id=self.get_test_user_2_group_id(), - block_id=doc_id, - type=get_access_type_id("view"), - ).one() + ba = ( + db.session.execute( + select(BlockAccess).filter_by( + usergroup_id=self.get_test_user_2_group_id(), + block_id=doc_id, + type=get_access_type_id("view"), + ) + ) + .scalars() + .one() + ) ba.accessible_to -= timedelta(days=2) db.session.commit() self.get( diff --git a/timApp/tests/server/test_jsrunner.py b/timApp/tests/server/test_jsrunner.py index 14f3d65349..3b4a0535cd 100644 --- a/timApp/tests/server/test_jsrunner.py +++ b/timApp/tests/server/test_jsrunner.py @@ -1,8 +1,10 @@ """Server tests for jsrunner plugin.""" import json from datetime import datetime, timezone +from select import select import requests +from sqlalchemy import func from timApp.answer.answer import Answer from timApp.auth.accesstype import AccessType @@ -1147,9 +1149,9 @@ def test_global_field(self): a = Answer(content=json.dumps({"c": "a"}), valid=True, task_id=f"{d.id}.GLO_a") self.test_user_1.answers.append(a) db.session.commit() - total_answer_count_before = Answer.query.count() + total_answer_count_before = db.session.scalar(select(func.count(Answer.id))) self.do_jsrun(d) - total_answer_count_after = Answer.query.count() + total_answer_count_after = db.session.scalar(select(func.count(Answer))) self.assertEqual(1, total_answer_count_after - total_answer_count_before) self.verify_answer_content( f"{d.id}.GLO_a", "c", "ab", self.test_user_1, expected_count=2 diff --git a/timApp/tests/server/test_lecture.py b/timApp/tests/server/test_lecture.py index 0bdf4c01a3..22697268d0 100644 --- a/timApp/tests/server/test_lecture.py +++ b/timApp/tests/server/test_lecture.py @@ -1,7 +1,6 @@ import datetime import json from time import sleep -from typing import Optional import dateutil.parser @@ -267,14 +266,14 @@ def test_lecture(self): new_end_time = dateutil.parser.parse(resp["question_end_time"]) self.assertTrue(original_end_time > new_end_time) - sp = Showpoints.query.get(aid) + sp = db.session.get(Showpoints, aid) self.assertIsNone(sp) self.login_test1() self.post("/showAnswerPoints", query_string=dict(asked_id=aid)) db.session.remove() - sp = Showpoints.query.get(aid) + sp = db.session.get(Showpoints, aid) self.assertIsNotNone(sp) resp = self.get_updates(doc.id, msg_id, True, aid) @@ -325,7 +324,7 @@ def test_lecture(self): q = get_asked_question(aid2) db.session.refresh(q) self.assertFalse(q.is_running) - l: Lecture = Lecture.query.get(lecture_id) + l: Lecture = db.session.get(Lecture, lecture_id) self.assertEqual(1, len(l.running_questions)) self.json_post("/extendLecture", {}, expect_status=400) diff --git a/timApp/tests/server/test_macros.py b/timApp/tests/server/test_macros.py index f7d7b98461..66a7ebc1a4 100644 --- a/timApp/tests/server/test_macros.py +++ b/timApp/tests/server/test_macros.py @@ -73,7 +73,7 @@ def test_user_macros(self): ) p, _ = Plugin.from_task_id( f"{d.id}.test", - UserContext.from_one_user(User.query.get(TEST_USER_1_ID)), + UserContext.from_one_user(db.session.get(User, TEST_USER_1_ID)), default_view_ctx, ) self.assertEqual("testuser1 and Test user 1", p.values["header"]) @@ -87,7 +87,7 @@ def test_user_macros(self): ) p, _ = Plugin.from_task_id( f"{d.id}.test", - UserContext.from_one_user(User.query.get(TEST_USER_2_ID)), + UserContext.from_one_user(db.session.get(User, TEST_USER_2_ID)), default_view_ctx, ) self.assertEqual("testuser2 and Test user 2", p.values["header"]) diff --git a/timApp/tests/server/test_notify.py b/timApp/tests/server/test_notify.py index 2f30f3afdc..33a2af8255 100644 --- a/timApp/tests/server/test_notify.py +++ b/timApp/tests/server/test_notify.py @@ -1,3 +1,5 @@ +from sqlalchemy import select + from timApp.auth.accesstype import AccessType from timApp.document.docinfo import DocInfo from timApp.document.randutils import random_id @@ -217,7 +219,7 @@ def test_answer_link_email_and_null_doc_text_after_processing(self): }, sent_mails_in_testing[-1], ) - pns = PendingNotification.query.filter_by(doc_id=d.id).all() + pns = db.session.execute(select(PendingNotification).filter_by(doc_id=d.id)).scalars().all() for p in pns: if isinstance(p, DocumentNotification): self.assertIsNone(p.text) diff --git a/timApp/tests/server/test_peer_review.py b/timApp/tests/server/test_peer_review.py index 7500a467fd..f1a48d2c46 100644 --- a/timApp/tests/server/test_peer_review.py +++ b/timApp/tests/server/test_peer_review.py @@ -1,4 +1,6 @@ """Server tests for peer review.""" +from sqlalchemy import select, delete + from timApp.answer.answer import Answer from timApp.peerreview.peerreview import PeerReview from timApp.tests.server.timroutetest import TimRouteTest @@ -41,15 +43,15 @@ def test_peer_review_generate(self): self.assertIn( "Not enough users to form pairs (0 but at least 2 users needed)", r ) - rq = PeerReview.query.filter_by(block_id=d.id) - self.assertEqual(0, len(rq.all())) + rq = select(PeerReview).filter_by(block_id=d.id) + self.assertEqual(0, len(db.session.execute(rq).scalars().all())) self.add_answer(d, "t", "x", user=self.test_user_1) db.session.commit() r = self.get(f"{url}?b={b}&size=1") self.assertIn( "Not enough users to form pairs (1 but at least 2 users needed)", r ) - self.assertEqual(0, len(rq.all())) + self.assertEqual(0, len(db.session.execute(rq).scalars().all())) self.add_answer(d, "t", "x", user=self.test_user_2) db.session.commit() r = self.get(f"{url}?b={b}&size=1") @@ -59,10 +61,14 @@ def test_peer_review_generate(self): def check_peerreview_rows_t(): prs: list[PeerReview] = ( - PeerReview.query.filter_by(block_id=d.id) - .order_by( - PeerReview.reviewer_id, + db.session.execute( + select(PeerReview) + .filter_by(block_id=d.id) + .order_by( + PeerReview.reviewer_id, + ) ) + .scalars() .all() ) self.assertEqual(2, len(prs)) @@ -75,7 +81,8 @@ def check_peerreview_rows_t(): # pairing without group works using everyone who answered the document check_peerreview_rows_t() - PeerReview.query.filter_by(block_id=d.id).delete() + + db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) d.document.add_setting("group", "testusers1") ug = UserGroup.create("testusers1") ug.users.append(self.test_user_1) @@ -85,7 +92,7 @@ def check_peerreview_rows_t(): self.get(f"{url}?b={b}&size=1") # pairing with group ignores group members who haven't answered the document check_peerreview_rows_t() - PeerReview.query.filter_by(block_id=d.id).delete() + db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) self.add_answer(d, "t", "x", user=self.test_user_3) ug = UserGroup.create("testuser3isnothere") ug.users.append(self.test_user_1) @@ -95,14 +102,14 @@ def check_peerreview_rows_t(): self.get(f"{url}?b={b}&size=1") # pairing with group setting ignores users who answered but aren't in the group check_peerreview_rows_t() - PeerReview.query.filter_by(block_id=d.id).delete() + db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) db.session.commit() dt = self.create_translation(d, lang="en-GB") tr_url = dt.get_url_for_view("review") self.get(f"{tr_url}?b={dt.document.get_paragraphs()[1].id}&size=1") # Peer-review generation works for one task in translated document check_peerreview_rows_t() - PeerReview.query.filter_by(block_id=d.id).delete() + db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) db.session.commit() self.get( f"{url}?b={pars[3].id}&size=1", @@ -122,10 +129,14 @@ def check_peerreview_rows_t(): d.document.add_setting("group", "testusers1") self.get(f"{url}?area=rev") prs: list[PeerReview] = ( - PeerReview.query.filter_by(block_id=d.id) - .order_by( - PeerReview.reviewer_id, + db.session.execute( + select(PeerReview) + .filter_by(block_id=d.id) + .order_by( + PeerReview.reviewer_id, + ) ) + .scalars() .all() ) @@ -146,14 +157,14 @@ def check_area_prs(): # Testuser1 and Testuser3 answered only to some tasks in the area => PR rows are still generated for every task check_area_prs() - PeerReview.query.filter_by(block_id=d.id).delete() + db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) db.session.commit() self.get(f"{tr_url}?area=rev") # Peer generation works for an area in translated document check_area_prs() - PeerReview.query.filter_by(block_id=d.id).delete() - all_answers = Answer.query.all() + db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) + all_answers = db.session.execute(select(Answer)).scalars().all() for a in all_answers: a.users_all = [] db.session.commit() diff --git a/timApp/tests/server/test_plugins.py b/timApp/tests/server/test_plugins.py index b9391d4d94..809cb41200 100644 --- a/timApp/tests/server/test_plugins.py +++ b/timApp/tests/server/test_plugins.py @@ -8,6 +8,7 @@ import dateutil.parser from lxml import html from lxml.html import HtmlElement +from sqlalchemy import select, func from timApp.answer.answer import Answer from timApp.answer.answer_models import AnswerUpload @@ -554,9 +555,10 @@ def test_broken_upload(self): self.do_plugin_upload(d, "test", "test.txt", f"{d.id}.testupload", "testupload") self.get(f"/uploads/{d.id}/testupload/testuser1/1/test.txt") a = ( - Answer.query.filter_by(task_id=f"{d.id}.testupload") + select(Answer) + .filter_by(task_id=f"{d.id}.testupload") .join(AnswerUpload) - .with_entities(AnswerUpload) + .with_only_columns(AnswerUpload) .first() ) @@ -1212,7 +1214,7 @@ def check_save_points( expect_content=expect_content, ) if expect_status == 200: - a = Answer.query.get(answer_id) + a = db.session.get(Answer, answer_id) self.assertEqual( float(points) if points not in ("", None) else None, a.points ) @@ -1475,7 +1477,12 @@ def test_answer_rename(self): p2 = Plugin.from_paragraph(d.document.get_paragraphs()[1], default_view_ctx) self.post_answer(p.type, p.task_id.doc_task, [True, False, False]) self.post_answer(p.type, p.task_id.doc_task, [True, True, False]) - self.assertEqual(2, Answer.query.filter_by(task_id=p.task_id.doc_task).count()) + self.assertEqual( + 2, + db.session.scalar( + select(func.count(Answer.id)).filter_by(task_id=p.task_id.doc_task) + ), + ) self.get( f"/renameAnswers/{p.task_id.task_name}/ä/{d.id}", expect_status=400, @@ -1492,8 +1499,18 @@ def test_answer_rename(self): "error": "The new name conflicts with 2 other answers with the same task name." }, ) - self.assertEqual(0, Answer.query.filter_by(task_id=p.task_id.doc_task).count()) - self.assertEqual(2, Answer.query.filter_by(task_id=f"{d.id}.t_new").count()) + self.assertEqual( + 0, + db.session.scalar( + select(func.count(Answer.id)).filter_by(task_id=p.task_id.doc_task) + ), + ) + self.assertEqual( + 2, + db.session.scalar( + select(func.count(Answer)).filter_by(task_id=f"{d.id}.t_new") + ), + ) self.post_answer(p2.type, p2.task_id.doc_task, [True, True, False]) self.get( f"/renameAnswers/t_new/{p2.task_id.task_name}/{d.id}", @@ -1518,7 +1535,12 @@ def test_answer_rename(self): query_string={"force": "true"}, expect_content={"modified": 2, "conflicts": 1}, ) - self.assertEqual(3, Answer.query.filter_by(task_id=p2.task_id.doc_task).count()) + self.assertEqual( + 3, + db.session.scalar( + select(func.count(Answer.id)).filter_by(task_id=p2.task_id.doc_task) + ), + ) self.login_test2() self.get( f"/renameAnswers/t_new/{p2.task_id.task_name}/{d.id}", expect_status=403 @@ -1547,7 +1569,7 @@ def test_save_teachers_fix(self): self.login_test2() a = self.post_answer("mmcq", f"{did}.t", [False, False, False]) aid = a["savedNew"] - a: Answer = Answer.query.get(aid) + a: Answer = db.session.get(Answer, aid) self.assertIsNone(a.saver) self.login_test1() @@ -1560,10 +1582,10 @@ def test_save_teachers_fix(self): user_id=self.test_user_2.id, answer_id=aid, )["savedNew"] - a: Answer = Answer.query.get(fix_id) + a: Answer = db.session.get(Answer, fix_id) self.assertEqual(1, len(a.users_all)) self.assertEqual(a.saver, self.current_user) - a: Answer = Answer.query.get(aid) + a: Answer = db.session.get(Answer, aid) self.assertEqual(1, len(a.users_all)) def test_pali(self): @@ -1859,16 +1881,16 @@ def test_taskid_field(self): r = self.post_answer("pali", f"{d.id}.t.points", user_input={"userword": 4}) aid = r["savedNew"] self.assertIsInstance(aid, int) - a = Answer.query.get(aid) + a = db.session.get(Answer, aid) self.assertEqual(4, a.points) r = self.post_answer("pali", f"{d.id}.t.points", user_input={"userword": 5}) self.assertIsNone(r["savedNew"]) - a = Answer.query.get(aid) + a = db.session.get(Answer, aid) self.assertEqual(5, a.points) r = self.post_answer("pali", f"{d.id}.t.points", user_input={"userword": "6"}) self.assertIsNone(r["savedNew"]) - a = Answer.query.get(aid) + a = db.session.get(Answer, aid) self.assertEqual(6, a.points) r = self.post_answer( @@ -1910,7 +1932,7 @@ def test_taskid_field(self): ) r = self.post_answer("pali", f"{d.id}.t.points", user_input={"userword": None}) self.assertIsNone(r["savedNew"]) - a = Answer.query.get(aid) + a = db.session.get(Answer, aid) self.assertEqual(None, a.points) # ensure these routes won't throw exceptions diff --git a/timApp/tests/server/test_plugins_preamble.py b/timApp/tests/server/test_plugins_preamble.py index 9bb1591eef..ed1161181f 100644 --- a/timApp/tests/server/test_plugins_preamble.py +++ b/timApp/tests/server/test_plugins_preamble.py @@ -6,6 +6,7 @@ from timApp.document.viewcontext import default_view_ctx from timApp.plugin.plugin import Plugin from timApp.tests.server.timroutetest import TimRouteTest +from timApp.timdb.sqa import db class PluginPreambleTest(TimRouteTest): @@ -34,7 +35,7 @@ def run_plugin_in_preamble(self, doc_path: str, create_preamble_translation=True plug = Plugin.from_paragraph(par, default_view_ctx) self.assertEqual(f"{d.id}.t", plug.task_id.doc_task) resp = self.post_answer(plug.type, plug.task_id.extended, [True]) - a: Answer = Answer.query.get(resp["savedNew"]) + a: Answer = db.session.get(Answer, resp["savedNew"]) self.assertEqual(1, a.points) self.assertEqual(f"{d.id}.t", a.task_id) self.get_state(a.id) @@ -56,7 +57,7 @@ def run_plugin_in_preamble(self, doc_path: str, create_preamble_translation=True [False], ref_from=(tr.id, tr.document.get_paragraphs()[0].get_id()), ) - a: Answer = Answer.query.get(resp["savedNew"]) + a: Answer = db.session.get(Answer, resp["savedNew"]) self.assertEqual(1 if create_preamble_translation else 0, a.points) self.assertEqual(f"{d.id}.t", a.task_id) self.check_plugin_ref_correct( @@ -107,7 +108,7 @@ def run_referenced_plugin_in_preamble( [True], ref_from=(d.id, d.document.get_paragraphs()[0].get_id()), ) - a: Answer = Answer.query.get(resp["savedNew"]) + a: Answer = db.session.get(Answer, resp["savedNew"]) self.assertEqual(1, a.points) self.assertEqual(plug.task_id.doc_task, a.task_id) @@ -125,7 +126,7 @@ def run_referenced_plugin_in_preamble( [False], ref_from=(tr.id, tr.document.get_paragraphs()[0].get_id()), ) - a: Answer = Answer.query.get(resp["savedNew"]) + a: Answer = db.session.get(Answer, resp["savedNew"]) self.assertEqual(0, a.points) self.assertEqual(plug.task_id.doc_task, a.task_id) self.check_plugin_ref_correct(tr, plugin_doc, plugin_par, preamble_doc=tr_p) diff --git a/timApp/tests/server/test_readings.py b/timApp/tests/server/test_readings.py index 6a546c6948..bcd8ceb5a6 100644 --- a/timApp/tests/server/test_readings.py +++ b/timApp/tests/server/test_readings.py @@ -1,12 +1,14 @@ from datetime import timedelta from lxml.cssselect import CSSSelector +from sqlalchemy import select, func from timApp.document.docinfo import DocInfo from timApp.readmark.readings import get_readings, get_read_expiry_condition from timApp.readmark.readparagraph import ReadParagraph from timApp.readmark.readparagraphtype import ReadParagraphType from timApp.tests.server.timroutetest import TimRouteTest +from timApp.timdb.sqa import db readline_selector = CSSSelector("div.readline") @@ -21,7 +23,7 @@ class ReadingsTest(TimRouteTest): def test_readings_normal(self): self.login_test1() doc = self.create_doc(initial_par=["test", "test2", "test3"]) - q = ReadParagraph.query.filter_by(doc_id=doc.id) + stmt = select(func.count(ReadParagraph.id)).filter_by(doc_id=doc.id) pars = doc.document.get_paragraphs() self.check_readlines(self.get_readings(doc), (UNREAD, UNREAD, UNREAD)) @@ -38,12 +40,12 @@ def test_readings_normal(self): self.get_readings(doc), (READ, MODIFIED, PAR_CLICK_MODIFIED) ) self.mark_as_read(doc, pars[2].get_id()) - self.assertEqual(q.count(), 4) + self.assertEqual(db.session.scalar(stmt), 4) self.check_readlines( self.get_readings(doc), (READ, MODIFIED, PAR_CLICK_MODIFIED + " " + READ) ) self.mark_as_unread(doc, pars[2].get_id()) - self.assertEqual(q.count(), 3) + self.assertEqual(db.session.scalar(stmt), 3) self.check_readlines( self.get_readings(doc), (READ, MODIFIED, PAR_CLICK_MODIFIED) ) @@ -52,7 +54,6 @@ def test_readings_group(self): self.login_test1() self.login_test2(add=True) doc = self.create_doc(initial_par=["test", "test2", "test3", "test4"]) - q = ReadParagraph.query.filter_by(doc_id=doc.id) self.check_readlines(self.get_readings(doc), (UNREAD, UNREAD, UNREAD, UNREAD)) pars = doc.document.get_paragraphs() self.mark_as_read(doc, pars[0].get_id()) @@ -82,7 +83,12 @@ def test_readings_group(self): self.check_readlines(self.get_readings(doc), (READ, READ, UNREAD, UNREAD)) self.login_test1() self.check_readlines(self.get_readings(doc), (READ, READ, MODIFIED, UNREAD)) - self.assertEqual(q.count(), 7) + self.assertEqual( + db.session.scalar( + select(func.count(ReadParagraph.id)).filter_by(doc_id=doc.id) + ), + 7, + ) self.get( f"/read/stats/{doc.id}", @@ -187,10 +193,10 @@ def test_mark_all_read(self): d = self.create_doc(initial_par=["1", "2"]) self.json_put(f"/read/{d.id}") self.check_readlines(self.get_readings(d), (READ, READ)) - q = ReadParagraph.query.filter_by(doc_id=d.id) - self.assertEqual(q.count(), 2) + stmt = select(func.count(ReadParagraph.id)).filter_by(doc_id=d.id) + self.assertEqual(db.session.scalar(stmt), 2) self.json_put(f"/read/{d.id}") - self.assertEqual(q.count(), 2) + self.assertEqual(db.session.scalar(stmt), 2) def test_expiry(self): self.login_test1() diff --git a/timApp/tests/server/test_self_expire.py b/timApp/tests/server/test_self_expire.py index b422e8fa7e..c2ad84b871 100644 --- a/timApp/tests/server/test_self_expire.py +++ b/timApp/tests/server/test_self_expire.py @@ -1,3 +1,5 @@ +from sqlalchemy import select + from timApp.answer.answer import Answer from timApp.auth.accesstype import AccessType from timApp.document.docentry import DocEntry @@ -68,7 +70,11 @@ def test_self_expire_field(self): }, ) - ans: list[Answer] = Answer.query.filter_by(task_id=f"{d.id}.test").all() + ans: list[Answer] = ( + db.session.execute(select(Answer).filter_by(task_id=f"{d.id}.test")) + .scalars() + .all() + ) self.assertEqual(len(ans), 1) self.assertEqual([u.name for u in ans[0].users_all], [self.test_user_2.name]) self.assertEqual(ans[0].content, '{"c": "1"}') diff --git a/timApp/tests/server/test_signup.py b/timApp/tests/server/test_signup.py index 70dfb86e26..7c6f505643 100644 --- a/timApp/tests/server/test_signup.py +++ b/timApp/tests/server/test_signup.py @@ -2,6 +2,8 @@ from dataclasses import dataclass from unittest import mock +from sqlalchemy import select, delete + from timApp.auth.login import ( test_pws, create_or_update_user, @@ -111,7 +113,11 @@ def test_block_bot_signup(self): }, ) self.get("/") # refresh session - self.assertIsNone(NewUser.query.filter_by(email=bot_email).first()) + self.assertIsNone( + db.session.execute(select(NewUser).filter_by(email=bot_email).limit(1)) + .scalars() + .first() + ) for allowed_email in ("test@jyu.fi", "test@gmail.com"): self.json_post( @@ -122,15 +128,21 @@ def test_block_bot_signup(self): }, ) self.get("/") # refresh session - self.assertIsNotNone(NewUser.query.filter_by(email=allowed_email).first()) - NewUser.query.delete() + self.assertIsNotNone( + db.session.execute( + select(NewUser).filter_by(email=allowed_email).limit(1) + ) + .scalars() + .first() + ) + db.session.execute(delete(NewUser)) db.session.commit() def test_signup_case_insensitive(self): email = "SomeOneCase@example.com" self.json_post("/emailSignup", {"email": email}) self.assertEqual( - NewUser.query.with_entities(NewUser.email).all(), + db.session.execute(select(NewUser.email)).scalars().all(), [("someonecase@example.com",)], ) self.json_post( @@ -156,7 +168,7 @@ def test_signup_whitespace(self): email = "whitespace@example.com " self.json_post("/emailSignup", {"email": email}) self.assertEqual( - NewUser.query.with_entities(NewUser.email).all(), + db.session.execute(select(NewUser.email)).scalars().all(), [("whitespace@example.com",)], ) self.json_post( @@ -206,9 +218,9 @@ def test_login_case_insensitive(self): def test_signup(self): email = "testingsignup@example.com" self.json_post("/emailSignup", {"email": email}) - self.assertEqual(NewUser.query.with_entities(NewUser.email).all(), [(email,)]) + self.assertEqual(db.session.execute(select(NewUser.email)).scalars().all(), [(email,)]) self.json_post("/emailSignup", {"email": email}) - self.assertEqual(NewUser.query.with_entities(NewUser.email).all(), [(email,)]) + self.assertEqual(db.session.execute(select(NewUser.email)).scalars().all(), [(email,)]) self.json_post( "/emailSignupFinish", { @@ -221,7 +233,7 @@ def test_signup(self): expect_contains="registered", json_key="status", ) - self.assertEqual(NewUser.query.with_entities(NewUser.email).all(), []) + self.assertEqual(db.session.execute(select(NewUser.email)).scalars().all(), []) self.assertEqual("Testing Signup", self.current_user.real_name) self.assertEqual(UserOrigin.Email, self.current_user.origin) self.assertEqual(email, self.current_user.email) diff --git a/timApp/tests/server/test_tim_message.py b/timApp/tests/server/test_tim_message.py index 7b896460ec..e8cbd722b1 100644 --- a/timApp/tests/server/test_tim_message.py +++ b/timApp/tests/server/test_tim_message.py @@ -1,3 +1,5 @@ +from sqlalchemy import select + from timApp.auth.accesstype import AccessType from timApp.document.docentry import DocEntry from timApp.folder.folder import Folder @@ -6,6 +8,7 @@ InternalMessage, ) from timApp.tests.server.timroutetest import TimRouteTest +from timApp.timdb.sqa import db class UrlTest(TimRouteTest): @@ -126,11 +129,27 @@ def test_send_message(self): "/view/messages/tim-messages", expect_status=200 ) # tim-messages folder created successfully - display = InternalMessageDisplay.query.filter_by( - usergroup_id=self.get_test_user_1_group_id() - ).first() - msg = InternalMessage.query.filter_by(id=display.message_id).first() - msg_doc = DocEntry.query.filter_by(id=msg.doc_id).first() + display = ( + db.session.execute( + select(InternalMessageDisplay) + .filter_by(usergroup_id=self.get_test_user_1_group_id()) + .limit(1) + ) + .scalars() + .first() + ) + msg = ( + db.session.execute( + select(InternalMessage).filter_by(id=display.message_id).limit(1) + ) + .scalars() + .first() + ) + msg_doc = ( + db.session.execute(select(DocEntry).filter_by(id=msg.doc_id).limit(1)) + .scalars() + .first() + ) self.get( f"/view/{msg_doc.name}", expect_status=200 ) # document for message created successfully diff --git a/timApp/tests/server/test_translation.py b/timApp/tests/server/test_translation.py index 9945e79cf9..a6061a979c 100644 --- a/timApp/tests/server/test_translation.py +++ b/timApp/tests/server/test_translation.py @@ -1,5 +1,7 @@ from unittest.mock import patch, Mock +from sqlalchemy import select + from timApp.auth.accesstype import AccessType from timApp.document.docentry import DocEntry from timApp.document.docinfo import DocInfo @@ -83,6 +85,13 @@ def usage(self) -> Usage: class TranslationTest(TimTranslationTest): + def get_deepl_service(self) -> DeeplTranslationService: + return ( + db.session.execute(select(DeeplTranslationService).limit(1)) + .scalars() + .first() + ) + def test_translation_create(self): self.login_test1() doc = self.create_doc() @@ -450,7 +459,7 @@ def test_document_machine_translation_route(self): tr_json = self.json_post( f"/translate/{d.id}/{lang.lang_code}/{data}", {"doc_title": "title"} ) - tr_doc = Translation.query.get(tr_json["id"]).document + tr_doc = db.session.get(Translation, tr_json["id"]).document tr_doc.clear_mem_cache() mds = map(lambda x: x.md, tr_doc.get_paragraphs()) self.assertEqual("ooF", next(mds)) @@ -471,7 +480,7 @@ def test_document_machine_translation_route_no_api_key(self): """ ) d.lang_id = "orig" - data = DeeplTranslationService.query.first().service_name + data = self.get_deepl_service().service_name self.json_post( f"/translate/{d.id}/{lang.lang_code}/{data}", {"doc_title": "title"}, @@ -494,7 +503,7 @@ def test_document_machine_translation_route_forbidden(self): d.lang_id = "orig" self.logout() self.login_test2() - data = DeeplTranslationService.query.first().service_name + data = self.get_deepl_service().service_name self.json_post( f"/translate/{d.id}/{lang.lang_code}/{data}", {"doc_title": "title"}, @@ -559,7 +568,7 @@ def test_paragraph_machine_translation_route_no_api_key(self): tr = self.create_translation(d) tr_doc = tr.document id1, id2, id3, *_ = [x.id for x in tr_doc.get_paragraphs()] - data = DeeplTranslationService.query.first().service_name + data = self.get_deepl_service().service_name self.json_post( f"/translate/paragraph/{tr.id}/{id1}/{lang.lang_code}/{data}", expect_status=404, @@ -582,7 +591,7 @@ def test_paragraph_machine_translation_route_forbidden(self): self.login_test2() tr_doc = tr.document id1, id2, id3, *_ = [x.id for x in tr_doc.get_paragraphs()] - data = DeeplTranslationService.query.first().service_name + data = self.get_deepl_service().service_name self.json_post( f"/translate/paragraph/{tr.id}/{id1}/{lang.lang_code}/{data}", expect_status=403, @@ -677,7 +686,7 @@ def test_text_machine_translation_route_no_api_key(self): Baz qux [qux](www.example.com) """ - transl = DeeplTranslationService.query.first().service_name + transl = self.get_deepl_service().service_name data = { "originaltext": md, } @@ -702,7 +711,7 @@ def test_text_machine_translation_route_forbidden(self): Baz qux [qux](www.example.com) """ - transl = DeeplTranslationService.query.first().service_name + transl = self.get_deepl_service().service_name data = { "originaltext": md, } diff --git a/timApp/tests/server/test_user_sessions.py b/timApp/tests/server/test_user_sessions.py index 4738aee909..38179da630 100644 --- a/timApp/tests/server/test_user_sessions.py +++ b/timApp/tests/server/test_user_sessions.py @@ -1,3 +1,5 @@ +from sqlalchemy import select, delete, update + from timApp.auth.accesstype import AccessType from timApp.auth.session.model import UserSession from timApp.auth.session.util import verify_session_for @@ -14,8 +16,13 @@ def forget_session(self): def latest_session(self) -> UserSession: """Get latest session of Test User 1.""" return ( - UserSession.query.filter_by(user_id=self.test_user_1.id) - .order_by(UserSession.logged_in_at.desc()) + db.session.execute( + select(UserSession) + .filter_by(user_id=self.test_user_1.id) + .order_by(UserSession.logged_in_at.desc()) + .limit(1) + ) + .scalars() .first() ) @@ -25,10 +32,13 @@ def assert_sesion_expired_state( """Assert the state of Test User 1's sessions.""" self.assertEqual( [ - UserSession.query.filter_by( - user_id=self.test_user_1.id, - session_id=sess, + db.session.execute( + select(UserSession).filter_by( + user_id=self.test_user_1.id, + session_id=sess, + ) ) + .scalars() .one() .expired for sess in session_ids @@ -46,10 +56,12 @@ def test_session_basic(self): "SESSIONS_MAX_CONCURRENT_SESSIONS_PER_USER": None, } ): - UserSession.query.delete() + db.session.execute(delete(UserSession)) db.session.commit() self.login_test1(manual=True) - sessions: list[UserSession] = UserSession.query.all() + sessions: list[UserSession] = ( + db.session.execute(select(UserSession)).scalars().all() + ) self.assertEqual(len(sessions), 1) self.assertEqual(sessions[0].user.name, self.test_user_1.name) self.assertEqual(sessions[0].expired, False) @@ -63,7 +75,9 @@ def test_session_basic(self): ) self.logout() - sessions: list[UserSession] = UserSession.query.all() + sessions: list[UserSession] = ( + db.session.execute(select(UserSession)).scalars().all() + ) self.assertEqual(len(sessions), 1) self.assertEqual(sessions[0].user.name, self.test_user_1.name) self.assertEqual(sessions[0].expired, True) @@ -82,7 +96,7 @@ def test_session_access_block(self): "SESSIONS_MAX_CONCURRENT_SESSIONS_PER_USER": 1, } ): - UserSession.query.delete() + db.session.execute(delete(UserSession)) db.session.commit() self.login_test1(manual=True) @@ -100,7 +114,7 @@ def test_session_validity(self): "SESSIONS_MAX_CONCURRENT_SESSIONS_PER_USER": 1, } ): - UserSession.query.delete() + db.session.execute(delete(UserSession)) db.session.commit() self.login_test1(manual=True) @@ -166,9 +180,15 @@ def test_session_validity(self): "valid": True, }, ) - prev_session = UserSession.query.filter_by( - user_id=self.test_user_1.id, session_id=prev_id - ).first() + prev_session = ( + db.session.execute( + select(UserSession) + .filter_by(user_id=self.test_user_1.id, session_id=prev_id) + .limit(1) + ) + .scalars() + .first() + ) self.assertEqual( prev_session.expired, True, @@ -186,7 +206,7 @@ def test_session_verify_remote(self): "DIST_RIGHTS_RECEIVE_SECRET": "yyy", } ): - UserSession.query.delete() + db.session.execute(delete(UserSession)) db.session.commit() session_ids = [] @@ -246,8 +266,10 @@ def test_session_verify_remote(self): ) # Mark all sessions as not expired to test verification of the latest session - UserSession.query.filter_by(user_id=self.test_user_1.id).update( - {"expired_at": None} + db.session.execute( + update(UserSession) + .where(UserSession.user_id == self.test_user_1.id) + .values({"expired_at": None}) ) db.session.commit() @@ -275,7 +297,7 @@ def test_session_invalidate(self) -> None: "DIST_RIGHTS_RECEIVE_SECRET": "yyy", } ): - UserSession.query.delete() + db.session.execute(delete(UserSession)) db.session.commit() session_ids = [] @@ -347,7 +369,13 @@ def test_session_invalidate(self) -> None: ) for session_id in session_ids: - sess = UserSession.query.filter_by(session_id=session_id).first() + sess = ( + db.session.execute( + select(UserSession).filter_by(session_id=session_id).limit(1) + ) + .scalars() + .first() + ) sess.expired_at = None db.session.commit() diff --git a/timApp/tests/server/test_velp.py b/timApp/tests/server/test_velp.py index 71219bd07a..1d7b6a88e0 100644 --- a/timApp/tests/server/test_velp.py +++ b/timApp/tests/server/test_velp.py @@ -13,8 +13,7 @@ """ import json -from timApp.util.utils import get_current_time -from timApp.user.usergroup import UserGroup +from sqlalchemy import select from timApp.auth.accesshelper import get_doc_or_abort from timApp.auth.accesstype import AccessType @@ -25,6 +24,8 @@ from timApp.folder.folder import Folder from timApp.tests.server.timroutetest import TimRouteTest from timApp.timdb.sqa import db +from timApp.user.usergroup import UserGroup +from timApp.util.utils import get_current_time from timApp.velp.annotation import Annotation from timApp.velp.velp import create_new_velp, DEFAULT_PERSONAL_VELP_GROUP_NAME from timApp.velp.velp_models import ( @@ -365,11 +366,37 @@ def test_delete_velp_group(self): self.assertEqual(f"roskis/{g['name']}", deleted.path) # database should not contain any references to the velp group - vg = VelpGroup.query.filter_by(id=g["id"]).first() - v_in_g = VelpInGroup.query.filter_by(velp_group_id=g["id"]).all() - vg_sel = VelpGroupSelection.query.filter_by(velp_group_id=g["id"]).all() - vg_def = VelpGroupDefaults.query.filter_by(velp_group_id=g["id"]).all() - vg_in_doc = VelpGroupsInDocument.query.filter_by(velp_group_id=g["id"]).all() + vg = ( + db.session.execute(select(VelpGroup).filter_by(id=g["id"]).limit(1)) + .scalars() + .first() + ) + v_in_g = ( + db.session.execute(select(VelpInGroup).filter_by(velp_group_id=g["id"])) + .scalars() + .all() + ) + vg_sel = ( + db.session.execute( + select(VelpGroupSelection).filter_by(velp_group_id=g["id"]) + ) + .scalars() + .all() + ) + vg_def = ( + db.session.execute( + select(VelpGroupDefaults).filter_by(velp_group_id=g["id"]) + ) + .scalars() + .all() + ) + vg_in_doc = ( + db.session.execute( + select(VelpGroupsInDocument).filter_by(velp_group_id=g["id"]) + ) + .scalars() + .all() + ) self.assertEqual(None, vg) self.assertEqual(0, len(v_in_g)) @@ -388,11 +415,37 @@ def test_delete_velp_group(self): self.assertEqual(f"roskis/{g2['name']}", deleted.path) # database should not contain any references to the velp group - vg2 = VelpGroup.query.filter_by(id=g2["id"]).first() - v_in_g2 = VelpInGroup.query.filter_by(velp_group_id=g2["id"]).all() - vg_sel2 = VelpGroupSelection.query.filter_by(velp_group_id=g2["id"]).all() - vg_def2 = VelpGroupDefaults.query.filter_by(velp_group_id=g2["id"]).all() - vg_in_doc2 = VelpGroupsInDocument.query.filter_by(velp_group_id=g2["id"]).all() + vg2 = ( + db.session.execute(select(VelpGroup).filter_by(id=g2["id"]).limit(1)) + .scalars() + .first() + ) + v_in_g2 = ( + db.session.execute(select(VelpInGroup).filter_by(velp_group_id=g2["id"])) + .scalars() + .all() + ) + vg_sel2 = ( + db.session.execute( + select(VelpGroupSelection).filter_by(velp_group_id=g2["id"]) + ) + .scalars() + .all() + ) + vg_def2 = ( + db.session.execute( + select(VelpGroupDefaults).filter_by(velp_group_id=g2["id"]) + ) + .scalars() + .all() + ) + vg_in_doc2 = ( + db.session.execute( + select(VelpGroupsInDocument).filter_by(velp_group_id=g2["id"]) + ) + .scalars() + .all() + ) self.assertEqual(None, vg2) self.assertEqual(0, len(v_in_g2)) @@ -817,7 +870,6 @@ def test_velp_group_clear_permissions(self): self.get(g_doc.url, expect_status=403) def test_velp_group_mass_edit_permissions(self): - self.login_test1() d1 = self.create_doc(title="test velp group permissions") d2 = self.create_doc(title="test velp group permissions 2") diff --git a/timApp/tests/server/test_verification.py b/timApp/tests/server/test_verification.py index af9b37ea37..cb1b2562c8 100644 --- a/timApp/tests/server/test_verification.py +++ b/timApp/tests/server/test_verification.py @@ -1,9 +1,11 @@ +from sqlalchemy import select + from timApp.messaging.messagelist.listinfo import Channel from timApp.notification.send_email import sent_mails_in_testing from timApp.tests.server.timroutetest import TimRouteTest from timApp.tim_app import app from timApp.timdb.sqa import db -from timApp.user.usercontact import ContactOrigin, UserContact +from timApp.user.usercontact import UserContact from timApp.user.verification.verification import ContactAddVerification @@ -29,10 +31,15 @@ def add_email(email: str) -> tuple[dict, str]: self.assertIsNotNone(u.get_contact(Channel.EMAIL, email)) verification = ( - ContactAddVerification.query.join(UserContact) - .filter( - (ContactAddVerification.user == u) & (UserContact.contact == email) + db.session.execute( + select(ContactAddVerification) + .join(UserContact) + .filter( + (ContactAddVerification.user == u) + & (UserContact.contact == email) + ) ) + .scalars() .one() ) @@ -124,11 +131,15 @@ def test_custom_verify_template(self): ) verification = ( - ContactAddVerification.query.join(UserContact) - .filter( - (ContactAddVerification.user == self.test_user_1) - & (UserContact.contact == "someotheremail1@example.com") + db.session.execute( + select(ContactAddVerification) + .join(UserContact) + .filter( + (ContactAddVerification.user == self.test_user_1) + & (UserContact.contact == "someotheremail1@example.com") + ) ) + .scalars() .one() ) diff --git a/timApp/tests/server/timroutetest.py b/timApp/tests/server/timroutetest.py index 83911b3295..9589c1a761 100644 --- a/timApp/tests/server/timroutetest.py +++ b/timApp/tests/server/timroutetest.py @@ -19,6 +19,7 @@ from lxml import html from lxml.html import HtmlElement from requests import PreparedRequest +from sqlalchemy import select import timApp.tim from timApp.answer.answer import Answer @@ -32,9 +33,8 @@ PREAMBLE_FOLDER_NAME, DEFAULT_PREAMBLE_DOC, ) -from tim_common.timjsonencoder import TimJsonEncoder -from timApp.document.translation.translation import Translation from timApp.document.translation.language import Language +from timApp.document.translation.translation import Translation from timApp.item.item import Item from timApp.item.routes import create_item_direct from timApp.messaging.messagelist.listinfo import ArchiveType @@ -54,6 +54,7 @@ from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.util.utils import remove_prefix +from tim_common.timjsonencoder import TimJsonEncoder def load_json(resp: Response): @@ -957,7 +958,7 @@ def create_translation( expect_status=expect_status, **kwargs, ) - return Translation.query.get(j["id"]) if expect_status == 200 else None + return db.session.get(Translation, j["id"]) if expect_status == 200 else None def assert_content(self, element: HtmlElement, expected: list[str]): pars = get_content(element) @@ -1389,9 +1390,11 @@ def create_list( } }, ) - message_list: MessageListModel = MessageListModel.query.filter_by( - name=name - ).one() + message_list: MessageListModel = ( + db.session.execute(select(MessageListModel).filter_by(name=name)) + .scalars() + .one() + ) return manage_doc, message_list def trigger_mailman_event(self, event: MessageEventType) -> None: diff --git a/timApp/tim_celery.py b/timApp/tim_celery.py index 951f4eef27..7b6356b05b 100644 --- a/timApp/tim_celery.py +++ b/timApp/tim_celery.py @@ -14,6 +14,7 @@ from celery.signals import after_setup_logger from celery.utils.log import get_task_logger from marshmallow import EXCLUDE, ValidationError +from sqlalchemy import delete from timApp.answer.routes import post_answer_impl, AnswerRouteResult from timApp.document.usercontext import UserContext @@ -252,14 +253,19 @@ def cleanup_verifications(): now = get_current_time() end_time_unreacted = now - timedelta(seconds=max_unreacted_interval) end_time_reacted = now - timedelta(seconds=max_reacted_interval) - Verification.query.filter( - (Verification.requested_at < end_time_unreacted) - & (Verification.reacted_at == None) - ).delete() - Verification.query.filter( - (Verification.requested_at < end_time_reacted) - & (Verification.reacted_at != None) - ).delete() + db.session.execute( + delete(Verification).where( + (Verification.requested_at < end_time_unreacted) + & (Verification.reacted_at == None) + ) + ) + db.session.execute( + delete(Verification).where( + (Verification.requested_at < end_time_reacted) + & (Verification.reacted_at != None) + ) + ) + db.session.commit() diff --git a/timApp/upload/upload.py b/timApp/upload/upload.py index 5cbe6d28e2..6e11c23e56 100644 --- a/timApp/upload/upload.py +++ b/timApp/upload/upload.py @@ -13,7 +13,7 @@ from PIL.Image import DecompressionBombError, registered_extensions from flask import Blueprint, request, send_file, Response, url_for from img2pdf import convert -from sqlalchemy import case +from sqlalchemy import case, select from werkzeug.utils import secure_filename from timApp.auth.accesshelper import ( @@ -124,11 +124,16 @@ def get_pluginupload(relfilename: str) -> tuple[str, PluginUpload]: relfilename = check_and_format_filename(relfilename) block = ( - Block.query.filter( - (Block.description.startswith(relfilename)) - & (Block.type_id == BlockType.Upload.value) + db.session.execute( + select(Block) + .filter( + (Block.description.startswith(relfilename)) + & (Block.type_id == BlockType.Upload.value) + ) + .order_by(Block.description.desc()) + .limit(1) ) - .order_by(Block.description.desc()) + .scalars() .first() ) if not block or ( @@ -171,11 +176,15 @@ def get_multiple_pluginuploads(relfilenames: list[str]) -> list[PluginUpload]: value=Block.description, ) blocks: list[Block] = ( - Block.query.filter( - (Block.description.in_(filenames)) - & (Block.type_id == BlockType.Upload.value) + db.session.execute( + select(Block) + .filter( + (Block.description.in_(filenames)) + & (Block.type_id == BlockType.Upload.value) + ) + .order_by(ordering) ) - .order_by(ordering) + .scalars() .all() ) if len(blocks) < len( @@ -641,7 +650,7 @@ def get_reviewcanvas_pdf(user_name: str, doc_id: int, task_id: str, answer_id: i "Some images could no longer be found, please delete the broken images first" ) byte_images = [] - for (block, file) in zip(blocks, files): + for block, file in zip(blocks, files): img = Image.open(io.BytesIO(block.data)) try: rotation = file["rotation"] diff --git a/timApp/upload/uploadedfile.py b/timApp/upload/uploadedfile.py index 1b3eb5e209..06987f5ca2 100644 --- a/timApp/upload/uploadedfile.py +++ b/timApp/upload/uploadedfile.py @@ -4,6 +4,7 @@ from typing import Optional, NamedTuple, Union import magic +from sqlalchemy import select from werkzeug.utils import secure_filename from timApp.answer.answer_models import AnswerUpload @@ -49,15 +50,22 @@ def __init__(self, b: Block): @staticmethod def find_by_id(block_id: int) -> Optional["UploadedFile"]: - b: Block | None = Block.query.get(block_id) + b: Block | None = db.session.get(Block, block_id) return UploadedFile._wrap(b) @staticmethod def find_first_child(block: Block, name: str) -> Optional["UploadedFile"]: b = ( - Block.query.join(BlockAssociation, BlockAssociation.child == Block.id) - .filter((BlockAssociation.parent == block.id) & (Block.description == name)) - .order_by(Block.id.desc()) + db.session.execute( + select(Block) + .join(BlockAssociation, BlockAssociation.child == Block.id) + .filter( + (BlockAssociation.parent == block.id) & (Block.description == name) + ) + .order_by(Block.id.desc()) + .limit(1) + ) + .scalars() .first() ) return UploadedFile._wrap(b) diff --git a/timApp/user/contacts.py b/timApp/user/contacts.py index 203e4dc6e3..8749432d6c 100644 --- a/timApp/user/contacts.py +++ b/timApp/user/contacts.py @@ -1,7 +1,7 @@ from dataclasses import field from flask import Response -from sqlalchemy import func +from sqlalchemy import func, select from sqlalchemy.orm import load_only from timApp.auth.accesshelper import verify_logged_in @@ -53,9 +53,15 @@ def add_contact( raise RouteException( "The contact is already added but is pending verification" ) - verification = ContactAddVerification.query.filter_by( - contact=existing_contact_info, reacted_at=None - ).first() + verification = ( + db.session.execute( + select(ContactAddVerification) + .filter_by(contact=existing_contact_info, reacted_at=None) + .limit(1) + ) + .scalars() + .first() + ) if verification: resend_verification(verification, existing_contact_info.contact) else: @@ -121,15 +127,13 @@ def remove_contact( if contact.contact_origin != ContactOrigin.Custom: raise RouteException("Cannot remove managed contacts") - verified_emails_count = ( - db.session.query(func.count(UserContact.id)) - .filter_by( + verified_emails_count = db.session.scalar( + select(func.count(UserContact.id)).filter_by( user_id=user.id, channel=Channel.EMAIL, verified=True, contact_origin=ContactOrigin.Custom, ) - .scalar() ) if contact.verified and verified_emails_count == 1: @@ -168,21 +172,32 @@ def set_primary( if existing_contact.primary: json_response({"verify": False}) - primary_contact_exists = db.session.query( - UserContact.query.filter_by( - channel=channel, contact=contact, primary=PrimaryContact.true - ).exists() - ).scalar() + primary_contact_exists = ( + db.session.execute( + select(UserContact.id) + .filter_by(channel=channel, contact=contact, primary=PrimaryContact.true) + .limit(1) + ) + .scalars() + .first() + is not None + ) if primary_contact_exists: raise RouteException( "Another user already has this contact set to primary, please choose another contact" ) - existing_verification = SetPrimaryContactVerification.query.filter_by( - contact=existing_contact, - reacted_at=None, - ).first() + existing_verification = ( + db.session.execute( + select(SetPrimaryContactVerification).filter_by( + contact=existing_contact, + reacted_at=None, + ) + ) + .scalars() + .first() + ) if existing_verification: resend_verification(existing_verification) return json_response({"verify": True}) diff --git a/timApp/user/groups.py b/timApp/user/groups.py index 35e5574cfd..b329e30a31 100644 --- a/timApp/user/groups.py +++ b/timApp/user/groups.py @@ -3,6 +3,7 @@ from typing import Any from flask import Response +from sqlalchemy import select from timApp.auth.accesshelper import ( verify_admin, @@ -62,10 +63,20 @@ def verify_groupadmin( def get_uid_gid( group_name: str, usernames_or_emails: list[str] ) -> tuple[UserGroup, list[User]]: - users = User.query.filter( - User.name.in_(usernames_or_emails) | User.email.in_(usernames_or_emails) - ).all() - group = UserGroup.query.filter_by(name=group_name).first() + users = ( + db.session.execute( + select(User).filter( + User.name.in_(usernames_or_emails) | User.email.in_(usernames_or_emails) + ) + ) + .scalars() + .all() + ) + group = ( + db.sesion.execute(select(UserGroup).filter_by(name=group_name).limit(1)) + .scalars() + .first() + ) raise_group_not_found_if_none(group_name, group) return group, users @@ -128,7 +139,9 @@ def show_usergroups(username: str) -> Response: if not u: raise NotExist(USER_NOT_FOUND) return json_response( - u.get_groups(include_special=False).order_by(UserGroup.name).all() + db.session.execute(u.get_groups(include_special=False).order_by(UserGroup.name)) + .scalars() + .all() ) diff --git a/timApp/user/hakaorganization.py b/timApp/user/hakaorganization.py index 227d1c6f2c..8cea0a281f 100644 --- a/timApp/user/hakaorganization.py +++ b/timApp/user/hakaorganization.py @@ -1,8 +1,9 @@ from functools import lru_cache -from timApp.timdb.sqa import db - from flask import current_app +from sqlalchemy import select + +from timApp.timdb.sqa import db class HakaOrganization(db.Model): @@ -13,7 +14,11 @@ class HakaOrganization(db.Model): @staticmethod def get_or_create(name: str): - found = HakaOrganization.query.filter_by(name=name).first() + found = ( + db.session.execute(select(HakaOrganization).filter_by(name=name).limit(1)) + .scalars() + .first() + ) if not found: found = HakaOrganization(name=name) db.session.add(found) diff --git a/timApp/user/personaluniquecode.py b/timApp/user/personaluniquecode.py index 070a89b8d8..ad10dec522 100644 --- a/timApp/user/personaluniquecode.py +++ b/timApp/user/personaluniquecode.py @@ -1,7 +1,8 @@ -from typing import Optional - import re from dataclasses import dataclass +from typing import Optional + +from sqlalchemy import select from timApp.timdb.sqa import db from timApp.user.hakaorganization import HakaOrganization @@ -49,9 +50,14 @@ def find_by_code( code: str, org: str, codetype: str ) -> Optional["PersonalUniqueCode"]: return ( - PersonalUniqueCode.query.filter_by(code=code, type=codetype) - .join(HakaOrganization) - .filter_by(name=org) + db.session.execute( + select(PersonalUniqueCode) + .filter_by(code=code, type=codetype) + .join(HakaOrganization) + .filter_by(name=org) + .limit(1) + ) + .scalars() .first() ) diff --git a/timApp/user/preferences.py b/timApp/user/preferences.py index 851f7a362f..7ab1062ad2 100644 --- a/timApp/user/preferences.py +++ b/timApp/user/preferences.py @@ -5,9 +5,11 @@ import attr from flask import has_request_context, request +from sqlalchemy import select from timApp.document.docentry import DocEntry from timApp.item.item import Item +from timApp.timdb.sqa import db from timApp.user.settings.style_utils import resolve_themes from tim_common.html_sanitize import sanitize_css @@ -43,7 +45,11 @@ def theme_docs(self) -> list[DocEntry]: return [] ordering = {d: i for i, d in enumerate(self.style_doc_ids)} return sorted( - DocEntry.query.filter(DocEntry.id.in_(self.style_doc_ids)).all(), + db.session.execute( + select(DocEntry).filter(DocEntry.id.in_(self.style_doc_ids)) + ) + .scalars() + .all(), key=lambda d: ordering[d.id], ) diff --git a/timApp/user/settings/settings.py b/timApp/user/settings/settings.py index a351b6cf34..a92a85b858 100644 --- a/timApp/user/settings/settings.py +++ b/timApp/user/settings/settings.py @@ -2,9 +2,10 @@ from dataclasses import field from typing import Any -from flask import render_template, session, flash, Response +from flask import render_template, flash, Response from flask import request from jinja2 import TemplateNotFound +from sqlalchemy import select from timApp.admin.user_cli import do_soft_delete from timApp.answer.answer_models import AnswerUpload @@ -56,9 +57,11 @@ def verify_new_styles(curr_prefs: Preferences, new_prefs: Preferences) -> None: if not new_style_doc_ids: return - new_style_docs: list[DocEntry] = DocEntry.query.filter( - DocEntry.id.in_(new_style_doc_ids) - ).all() + new_style_docs: list[DocEntry] = ( + db.session.execute(select(DocEntry).filter(DocEntry.id.in_(new_style_doc_ids))) + .scalars() + .all() + ) if len(new_style_docs) != len(new_style_doc_ids): raise NotExist("Some style docs could not be found") @@ -118,18 +121,44 @@ def get_setting(name: str) -> Response: def get_user_info(u: User, include_doc_content: bool = False) -> dict[str, Any]: """Returns all data associated with a user.""" block_query = get_owned_objects_query(u) - docs = DocEntry.query.filter(DocEntry.id.in_(block_query)).all() - folders = Folder.query.filter(Folder.id.in_(block_query)).all() - images = Block.query.filter( - Block.id.in_(block_query) & (Block.type_id == BlockType.Image.value) - ).all() - files = Block.query.filter( - Block.id.in_(block_query) & (Block.type_id == BlockType.File.value) - ).all() + docs = ( + db.session.execute(select(DocEntry).filter(DocEntry.id.in_(block_query))) + .scalars() + .all() + ) + folders = ( + db.session.execute(select(Folder).filter(Folder.id.in_(block_query))) + .scalars() + .all() + ) + images = ( + db.session.execute( + select(Block).filter( + Block.id.in_(block_query) & (Block.type_id == BlockType.Image.value) + ) + ) + .scalars() + .all() + ) + files = ( + db.session.execute( + select(Block).filter( + Block.id.in_(block_query) & (Block.type_id == BlockType.File.value) + ) + ) + .scalars() + .all() + ) answers = u.answers.all() - answer_uploads = AnswerUpload.query.filter( - AnswerUpload.answer_id.in_([a.id for a in answers]) - ).all() + answer_uploads = ( + db.session.execute( + select(AnswerUpload).filter( + AnswerUpload.answer_id.in_([a.id for a in answers]) + ) + ) + .scalars() + .all() + ) answers_no_points = list(map(hide_points, answers)) answers_no_points = list(map(hide_points_modifier, answers_no_points)) for d in docs: diff --git a/timApp/user/settings/styles.py b/timApp/user/settings/styles.py index 101deb1eb4..ee22ff486c 100644 --- a/timApp/user/settings/styles.py +++ b/timApp/user/settings/styles.py @@ -2,10 +2,10 @@ from io import StringIO from os.path import getmtime from pathlib import Path -from typing import Optional import sass from flask import Response, current_app, flash +from sqlalchemy import select from timApp.auth.accesshelper import verify_logged_in, verify_view_access from timApp.auth.sessioninfo import get_current_user_object @@ -19,6 +19,7 @@ from timApp.document.usercontext import UserContext from timApp.document.viewcontext import default_view_ctx from timApp.item.partitioning import get_doc_version_hash +from timApp.timdb.sqa import db from timApp.user.settings.style_utils import ( stylesheets_folder, get_default_scss_gen_dir, @@ -367,7 +368,11 @@ def generate( """ verify_logged_in() - doc_entries: list[DocEntry] = DocEntry.query.filter(DocEntry.id.in_(docs)).all() + doc_entries: list[DocEntry] = ( + db.session.execute(select(DocEntry).filter(DocEntry.id.in_(docs))) + .scalars() + .all() + ) for doc in doc_entries: verify_view_access(doc) diff --git a/timApp/user/user.py b/timApp/user/user.py index 911967e139..e500dc6293 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -7,13 +7,14 @@ import filelock from flask import current_app, has_request_context -from sqlalchemy import func +from sqlalchemy import func, select, delete from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import Query, joinedload, defaultload +from sqlalchemy.orm import joinedload, defaultload from sqlalchemy.orm.collections import ( attribute_mapped_collection, ) from sqlalchemy.orm.strategy_options import loader_option +from sqlalchemy.sql import Select from timApp.answer.answer import Answer from timApp.answer.answer_models import UserAnswer @@ -234,8 +235,8 @@ def last_name_to_last(full_name: str | None): deleted_user_pattern = re.compile(rf".*{deleted_user_suffix}(_\d+)?$") -def user_query_with_joined_groups() -> Query: - return User.query.options(joinedload(User.groups)) +def user_query_with_joined_groups() -> Select: + return select(User).options(joinedload(User.groups)) class User(db.Model, TimeStampMixin, SCIMEntity): @@ -486,9 +487,11 @@ def update_email( self._email = new_email if prev_email != new_email: if create_contact: - new_primary = UserContact.query.filter_by( - user_id=self.id, channel=Channel.EMAIL, contact=new_email - ).first() + new_primary = db.session.scalars( + select(UserContact).filter_by( + user_id=self.id, channel=Channel.EMAIL, contact=prev_email + ) + ).one_or_none() if not new_primary: # If new primary contact does not exist for the email, create it # This is used mainly for CLI operations where email of the user is changed directly @@ -534,10 +537,14 @@ def scim_resource_type(self): @property def scim_extra_data(self): """Any extra data that should be returned in the SCIM API response.""" - email_contacts = UserContact.query.filter_by( - user=self, channel=Channel.EMAIL, verified=True + email_contacts_stmt = select(UserContact).filter_by( + user_id=self.id, channel=Channel.EMAIL, verified=True ) - return {"emails": [{"value": uc.contact} for uc in email_contacts]} + return { + "emails": [ + {"value": uc.contact} for uc in db.session.scalars(email_contacts_stmt) + ] + } def __repr__(self): return f"" @@ -584,7 +591,9 @@ def effective_real_groups(): locked_groups = get_locked_active_groups() if locked_groups is None: return effective_real_groups() - return UserGroup.query.filter(UserGroup.id.in_(locked_groups)).all() + return db.session.scalars( + select(UserGroup).filter(UserGroup.id.in_(locked_groups)) + ).all() @property def effective_group_ids(self): @@ -682,25 +691,27 @@ def create_with_group( @staticmethod def get_by_name(name: str) -> Optional["User"]: - return user_query_with_joined_groups().filter_by(name=name).first() + return db.session.scalars( + user_query_with_joined_groups().filter_by(name=name).limit(1) + ).first() @staticmethod def get_by_id(uid: int) -> Optional["User"]: - return user_query_with_joined_groups().get(uid) + return db.session.get(User, uid, options=[joinedload(User.groups)]) @staticmethod def get_by_email(email: str) -> Optional["User"]: if email is None: raise Exception("Tried to find an user by null email") - return user_query_with_joined_groups().filter_by(email=email).first() + return db.session.scalars( + user_query_with_joined_groups().filter_by(email=email).limit(1) + ).first() @staticmethod def get_by_email_case_insensitive(email: str) -> list["User"]: - return ( - user_query_with_joined_groups() - .filter(func.lower(User.email).in_([email])) - .all() - ) + return db.session.scalars( + user_query_with_joined_groups().filter(func.lower(User.email).in_([email])) + ).all() @staticmethod def get_by_email_case_insensitive_or_username( @@ -718,8 +729,10 @@ def get_by_email_case_insensitive_or_username( def verified_email_name_parts(self) -> list[str]: email_parts = [ uc.contact.split("@") - for uc in UserContact.query.filter_by( - user=self, channel=Channel.EMAIL, verified=True + for uc in db.session.scalars( + select(UserContact).filter_by( + user=self, channel=Channel.EMAIL, verified=True + ) ) ] return [parts[0].lower() for parts in email_parts] @@ -790,23 +803,26 @@ def _get_personal_folders(self) -> list[Folder]: group_condition = UserGroup.name == self.name else: group_condition = UserGroup.name == ANONYMOUS_GROUPNAME - return ( - Folder.query.join(BlockAccess, BlockAccess.block_id == Folder.id) + + stmt = ( + select(Folder) + .join(BlockAccess, BlockAccess.block_id == Folder.id) .join(UserGroup, UserGroup.id == BlockAccess.usergroup_id) .filter( (Folder.location == "users") & group_condition & (BlockAccess.type == AccessType.owner.value) ) - .with_entities(Folder) + .with_only_columns(Folder) .options( defaultload(Folder._block) .joinedload(Block.accesses) .joinedload(BlockAccess.usergroup) ) - .all() ) + return db.session.scalars(stmt).unique().all() + @cached_property def personal_folder_prop(self) -> Folder: folders = self._get_personal_folders() @@ -843,21 +859,21 @@ def set_prefs(self, prefs: Preferences): def get_groups( self, include_special: bool = True, include_expired: bool = True - ) -> Query: + ) -> Select: special_groups = [ANONYMOUS_GROUPNAME] if self.logged_in: special_groups.append(LOGGED_IN_GROUPNAME) filter_expr = UserGroupMember.user_id == self.id if not include_expired: filter_expr = filter_expr & membership_current - q = UserGroup.query.filter( - UserGroup.id.in_( - db.session.query(UserGroupMember.usergroup_id).filter(filter_expr) - ) + stmt = select(UserGroup).filter( + UserGroup.id.in_(select(UserGroupMember.usergroup_id).filter(filter_expr)) ) if include_special: - q = q.union(UserGroup.query.filter(UserGroup.name.in_(special_groups))) - return q + stmt = stmt.union( + select(UserGroup).filter(UserGroup.name.in_(special_groups)) + ) + return stmt def add_to_group( self, @@ -908,14 +924,15 @@ def get_contact( :param options: Additional DB load options. :return: UserContact if found, otherwise None. """ - q = UserContact.query.filter( + + stmt = select(UserContact).filter( (UserContact.user == self) & (UserContact.channel == channel) & (UserContact.contact == contact) ) if options: - q = q.options(*options) - return q.first() + stmt = stmt.options(*options) + return db.session.scalars(stmt).one_or_none() @staticmethod def get_scimuser() -> "User": @@ -1325,11 +1342,12 @@ def remove_access(self, block_id: int, access_type: str | AccessType) -> None: """Remove user's permissions to the specified item (block)""" if isinstance(access_type, AccessType): access_type = access_type.value - BlockAccess.query.filter_by( - block_id=block_id, - usergroup_id=self.get_personal_group().id, - type=get_access_type_id(access_type), - ).delete() + stmt = delete(BlockAccess).where( + (BlockAccess.block_id == block_id) + & (BlockAccess.usergroup_id == self.get_personal_group().id) + & (BlockAccess.type == get_access_type_id(access_type)) + ) + db.session.execute(stmt) def get_notify_settings(self, item: DocInfo | Folder) -> dict: # TODO: Instead of conversion, expose all notification types in UI @@ -1432,13 +1450,16 @@ def is_sisu_teacher(self) -> bool: if self.is_special: return False teacher_group_id = ( - db.session.query(ScimUserGroup.group_id) - .join(UserGroup) - .join(UserGroupMember) - .filter( - (UserGroupMember.user_id == self.id) - & ScimUserGroup.external_id.like("%-teachers") + db.session.execute( + select(ScimUserGroup.group_id) + .join(UserGroup) + .join(UserGroupMember) + .filter( + (UserGroupMember.user_id == self.id) + & ScimUserGroup.external_id.like("%-teachers") + ) ) + .scalars() .first() ) return teacher_group_id is not None @@ -1476,8 +1497,10 @@ def to_json(self, full: bool = False, contacts: bool = False) -> dict: external_ids: dict[int, str] = ( { s.group_id: s.external_id - for s in ScimUserGroup.query.filter( - ScimUserGroup.group_id.in_([g.id for g in self.groups]) + for s in db.session.scalars( + select(ScimUserGroup).filter( + ScimUserGroup.group_id.in_([g.id for g in self.groups]) + ) ).all() } if full diff --git a/timApp/user/usergroup.py b/timApp/user/usergroup.py index d344ab294d..290363e600 100644 --- a/timApp/user/usergroup.py +++ b/timApp/user/usergroup.py @@ -4,8 +4,10 @@ from typing import TYPE_CHECKING import attr +from sqlalchemy import select from sqlalchemy.orm import joinedload from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.sql import Select from timApp.auth.auth_models import BlockAccess from timApp.messaging.messagelist.messagelist_models import MessageListTimMember @@ -236,19 +238,35 @@ def get_by_external_id(name: str) -> UserGroup: @staticmethod def get_by_name(name) -> UserGroup: - return UserGroup.query.filter_by(name=name).first() + return ( + db.session.execute(select(UserGroup).filter_by(name=name).limit(1)) + .scalars() + .first() + ) @staticmethod def get_anonymous_group() -> UserGroup: - return UserGroup.query.filter_by(name=ANONYMOUS_GROUPNAME).one() + return ( + db.session.execute(select(UserGroup).filter_by(name=ANONYMOUS_GROUPNAME)) + .scalars() + .one() + ) @staticmethod def get_admin_group() -> UserGroup: - return UserGroup.query.filter_by(name=ADMIN_GROUPNAME).one() + return ( + db.session.execute(select(UserGroup).filter_by(name=ADMIN_GROUPNAME)) + .scalars() + .one() + ) @staticmethod def get_groupadmin_group() -> UserGroup: - return UserGroup.query.filter_by(name=GROUPADMIN_GROUPNAME).one() + return ( + db.session.execute(select(UserGroup).filter_by(name=GROUPADMIN_GROUPNAME)) + .scalars() + .one() + ) @staticmethod def get_organization_group(org: str) -> UserGroup: @@ -262,13 +280,24 @@ def get_haka_group() -> UserGroup: @staticmethod def get_organizations() -> list[UserGroup]: - return UserGroup.query.filter( - UserGroup.name.endswith(" users") & UserGroup.name.notin_(SPECIAL_GROUPS) - ).all() + return ( + db.session.execute( + select(UserGroup).filter( + UserGroup.name.endswith(" users") + & UserGroup.name.notin_(SPECIAL_GROUPS) + ) + ) + .scalars() + .all() + ) @staticmethod def get_teachers_group() -> UserGroup: - return UserGroup.query.filter_by(name=TEACHERS_GROUPNAME).one() + return ( + db.session.execute(select(UserGroup).filter_by(name=TEACHERS_GROUPNAME)) + .scalars() + .one() + ) @staticmethod def get_user_creator_group() -> UserGroup: @@ -289,7 +318,11 @@ def get_or_create_group(group_name: str) -> UserGroup: @staticmethod def get_logged_in_group() -> UserGroup: - return UserGroup.query.filter_by(name=LOGGED_IN_GROUPNAME).one() + return ( + db.session.execute(select(UserGroup).filter_by(name=LOGGED_IN_GROUPNAME)) + .scalars() + .one() + ) @lru_cache @@ -307,17 +340,21 @@ def get_admin_group_id() -> int: return UserGroup.get_admin_group().id -def get_usergroup_eager_query(): +def get_usergroup_eager_query() -> Select: from timApp.item.block import Block - return UserGroup.query.options( - joinedload(UserGroup.admin_doc).joinedload(Block.docentries) - ).options(joinedload(UserGroup.current_memberships)) + return ( + select(UserGroup) + .options(joinedload(UserGroup.admin_doc).joinedload(Block.docentries)) + .options(joinedload(UserGroup.current_memberships)) + ) def get_sisu_groups_by_filter(f) -> list[UserGroup]: gs: list[UserGroup] = ( - get_usergroup_eager_query().join(ScimUserGroup).filter(f).all() + db.session.execute(get_usergroup_eager_query().join(ScimUserGroup).filter(f)) + .scalars() + .all() ) return gs diff --git a/timApp/user/users.py b/timApp/user/users.py index 3266d76d97..b9744936dc 100644 --- a/timApp/user/users.py +++ b/timApp/user/users.py @@ -1,7 +1,6 @@ from collections import defaultdict -from typing import Optional -from sqlalchemy import func +from sqlalchemy import func, select from sqlalchemy.orm import joinedload from timApp.auth.accesstype import AccessType @@ -41,8 +40,9 @@ def get_rights_holders(block_id: int) -> RightsList: def get_rights_holders_all(block_ids: list[int], order_by=None): if not order_by: order_by = User.name - result: list[tuple[BlockAccess, UserGroup, User | None]] = ( - BlockAccess.query.options( + result: list[tuple[BlockAccess, UserGroup, User | None]] = db.session.execute( + select(BlockAccess) + .options( joinedload(BlockAccess.usergroup) .joinedload(UserGroup.admin_doc) .joinedload(Block.docentries) @@ -51,10 +51,9 @@ def get_rights_holders_all(block_ids: list[int], order_by=None): .filter(BlockAccess.block_id.in_(block_ids)) .join(UserGroup) .outerjoin(User, User.name == UserGroup.name) - .with_entities(BlockAccess, UserGroup, User) + .with_only_columns(BlockAccess, UserGroup, User) .order_by(order_by) - .all() - ) + ).all() results = defaultdict(list) for acc, ug, user in result: if user: @@ -105,7 +104,7 @@ def create_anonymous_user(name: str, real_name: str) -> User: """ - next_id = User.query.with_entities(func.min(User.id)).scalar() - 1 + next_id = db.session.scalar(select(func.min(User.id))) - 1 u, _ = User.create_with_group( UserInfo(username=name + str(abs(next_id)), full_name=real_name), uid=next_id ) diff --git a/timApp/user/userutils.py b/timApp/user/userutils.py index 6807591d4a..d026fd81c0 100644 --- a/timApp/user/userutils.py +++ b/timApp/user/userutils.py @@ -3,6 +3,7 @@ from enum import Enum import bcrypt +from sqlalchemy import select from timApp.auth.accesstype import AccessType from timApp.auth.auth_models import AccessTypeModel, BlockAccess @@ -12,6 +13,7 @@ from timApp.item.block import BlockType, Block from timApp.item.item import ItemBase from timApp.timdb.exceptions import TimDbException +from timApp.timdb.sqa import db from timApp.user.special_group_names import ANONYMOUS_GROUPNAME, ANONYMOUS_USERNAME from timApp.user.usergroup import UserGroup from timApp.util.utils import get_current_time @@ -63,7 +65,7 @@ def get_anon_user_id() -> int: def get_access_type_id(access_type: str) -> int: if not access_type_map: - result = AccessTypeModel.query.all() + result = db.session.execute(select(AccessTypeModel)).scalars().all() for row in result: access_type_map[row.name] = row.id return access_type_map[access_type] @@ -87,11 +89,19 @@ def expire_access( :param access_type: The kind of access. Possible values are listed in accesstype table. :return: The BlockAccess object if there was previous access. Also returns whether the access was expired before. """ - ba: BlockAccess | None = BlockAccess.query.filter_by( - type=access_type.value, - block_id=block.id, - usergroup_id=group.id, - ).first() + ba: BlockAccess | None = ( + db.session.execute( + select(BlockAccess) + .filter_by( + type=access_type.value, + block_id=block.id, + usergroup_id=group.id, + ) + .limit(1) + ) + .scalars() + .first() + ) if not ba: return None, False if ba.expired: diff --git a/timApp/user/verification/routes.py b/timApp/user/verification/routes.py index 82b7eff4bb..7ebad67e2f 100644 --- a/timApp/user/verification/routes.py +++ b/timApp/user/verification/routes.py @@ -1,6 +1,5 @@ -from typing import Optional - from flask import render_template, Response +from sqlalchemy import select from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound # type: ignore from timApp.auth.accesshelper import verify_logged_in @@ -24,9 +23,15 @@ def get_verification_data( if not verify_type_parsed: error = "Invalid verification type" - verification: Verification | None = Verification.query.filter_by( - token=verify_token, type=verify_type_parsed - ).first() + verification: Verification | None = ( + db.session.execute( + select(Verification) + .filter_by(token=verify_token, type=verify_type_parsed) + .limit(1) + ) + .scalars() + .first() + ) if not verification: error = "No verification found for the token and type" diff --git a/timApp/user/verification/verification.py b/timApp/user/verification/verification.py index d0a085f56b..bfcfd520e2 100644 --- a/timApp/user/verification/verification.py +++ b/timApp/user/verification/verification.py @@ -4,6 +4,7 @@ from typing import Optional from flask import render_template_string, url_for +from sqlalchemy import select from sqlalchemy.orm import load_only from timApp.document.docentry import DocEntry @@ -127,11 +128,19 @@ def approve(self) -> None: if not self.contact: return - current_primary = UserContact.query.filter_by( - user_id=self.user_id, - channel=self.contact.channel, - primary=PrimaryContact.true, - ).first() + current_primary = ( + db.session.execute( + select(UserContact) + .filter_by( + user_id=self.user_id, + channel=self.contact.channel, + primary=PrimaryContact.true, + ) + .limit(1) + ) + .scalars() + .first() + ) with db.session.no_autoflush: if current_primary: current_primary.primary = None @@ -141,8 +150,12 @@ def approve(self) -> None: # We update email directly since we already resolved the contact in previous steps u = ( - User.query.filter_by(id=self.user_id) - .options(load_only(User.email, User.id)) + db.session.execute( + select(User) + .filter_by(id=self.user_id) + .options(load_only(User.email, User.id)) + ) + .scalars() .one() ) old_email = u._email diff --git a/timApp/util/flask/search.py b/timApp/util/flask/search.py index 8ba72fdea7..018a1095f3 100644 --- a/timApp/util/flask/search.py +++ b/timApp/util/flask/search.py @@ -12,6 +12,7 @@ from flask import Blueprint, json, Request from flask import request +from sqlalchemy import select from sqlalchemy.orm import joinedload, defaultload from timApp.auth.accesshelper import has_view_access, verify_admin, has_edit_access @@ -23,6 +24,7 @@ from timApp.item.routes import get_document_relevance from timApp.timdb.dbaccess import get_files_path from timApp.timdb.exceptions import InvalidReferenceException +from timApp.timdb.sqa import db from timApp.user.user import User from timApp.util.flask.requesthelper import ( get_option, @@ -65,7 +67,9 @@ def get_subfolders(m: GetFoldersModel): root_path = m.folder if root_path == "": return json_response([]) - folders = Folder.query.filter(Folder.location.like(root_path + "%")).limit(50) + folders = db.session.execute( + select(Folder).filter(Folder.location.like(root_path + "%")).limit(50) + ).scalars() folders_viewable = [root_path] for folder in folders: if has_view_access(folder): @@ -545,7 +549,6 @@ def create_search_files(remove_deleted_pars=True) -> tuple[int, str]: ) as temp_tags_file, index_log_file_name.open( "w+", encoding="utf-8" ) as index_log_file: - current_doc, current_pars = None, [] for line in raw_file: @@ -727,10 +730,18 @@ def fetch_search_items(search_items: dict, search_folder: str) -> list[DocInfo]: :param search_folder: folder path that the search is limited to :return: list of DocInfo objects """ - doc_infos: list[DocInfo] = DocEntry.query.filter( - (DocEntry.id.in_(search_items.keys())) - & (DocEntry.name.like(search_folder + "%")) - ).options(joinedload(DocEntry._block).joinedload(Block.relevance)) + doc_infos: list[DocInfo] = ( + db.session.execute( + select(DocEntry) + .filter( + (DocEntry.id.in_(search_items.keys())) + & (DocEntry.name.like(search_folder + "%")) + ) + .options(joinedload(DocEntry._block).joinedload(Block.relevance)) + ) + .scalars() + .all() + ) return doc_infos diff --git a/timApp/util/get_fields.py b/timApp/util/get_fields.py index c5f9ea0fdc..35c32d4b97 100644 --- a/timApp/util/get_fields.py +++ b/timApp/util/get_fields.py @@ -12,7 +12,7 @@ import dateutil.parser from isodate import datetime_isoformat from marshmallow import missing -from sqlalchemy import func, true +from sqlalchemy import func, true, select from sqlalchemy.orm import lazyload, joinedload from timApp.answer.answer import Answer @@ -28,6 +28,7 @@ from timApp.document.viewcontext import ViewContext from timApp.plugin.plugin import find_task_ids, CachedPluginFinder from timApp.plugin.taskid import TaskId +from timApp.timdb.sqa import db from timApp.user.groups import verify_group_view_access from timApp.user.user import User, get_membership_end, get_membership_added from timApp.user.usergroup import UserGroup @@ -129,9 +130,13 @@ class RequestedGroups: include_all_answered: bool = False @staticmethod - def from_name_list(group_names: list[str]): + def from_name_list(group_names: list[str]) -> "RequestedGroups": return RequestedGroups( - groups=UserGroup.query.filter(UserGroup.name.in_(group_names)).all(), + groups=db.session.execute( + select(UserGroup).filter(UserGroup.name.in_(group_names)) + ) + .scalars() + .all(), include_all_answered=ALL_ANSWERED_WILDCARD in group_names, ) @@ -340,6 +345,7 @@ def get_fields_and_users( plugin_info_fields, d, doc_map, view_ctx, user_ctx ) + # FIXME: SQLAlchemy check behaviour sub = [] # For some reason, with 7 or more fields, executing the following query is very slow in PostgreSQL 9.5. # That's why we split the list of task ids in chunks of size 6 and merge the results. @@ -353,8 +359,12 @@ def get_fields_and_users( # Ensure user filter gets applied even if group filter is skipped in include_all_answered q = q.filter(user_filter) sub += ( - q.group_by(Answer.task_id, User.id) - .with_entities(func.max(Answer.id), User.id) + db.session.execute( + q.group_by(Answer.task_id, User.id).with_only_columns( + func.max(Answer.id), User.id + ) + ) + .scalars() .all() ) aid_uid_map = defaultdict(list) @@ -363,7 +373,7 @@ def get_fields_and_users( aid_uid_map[aid].append(uid) user_ids.add(uid) - q1 = User.query.join(UserGroup, join_relation).filter(group_filter) + q1 = select(User).join(UserGroup, join_relation).filter(group_filter) if requested_groups.include_all_answered: # if no group filter is given, attempt to get users that have valid answers only using the user # ids from previous query @@ -371,27 +381,38 @@ def get_fields_and_users( # Ensure that user filter gets applied even if group filter was None if user_filter is not None: id_filter = id_filter & user_filter - q2 = User.query.filter(id_filter) + q2 = select(User).filter(id_filter) q = q1.union(q2) else: q = q1 - q = q.with_entities(User).order_by(User.id).options(lazyload(User.groups)) + q = q.with_only_columns(User).order_by(User.id).options(lazyload(User.groups)) if member_filter_type != MembershipFilter.Current: q = q.options(joinedload(User.memberships)) - users: list[User] = q.all() + users: list[User] = db.session.execute(q).scalars().all() user_map = {} for u in users: user_map[u.id] = u global_taskids = [t for t in task_ids if t.is_global] global_answer_ids = ( - valid_answers_query(global_taskids) - .group_by(Answer.task_id) - .with_entities(func.max(Answer.id)) + db.session.execute( + valid_answers_query(global_taskids) + .group_by(Answer.task_id) + .with_only_columns(func.max(Answer.id)) + ) + .scalars() + .all() + ) + answs = ( + db.session.execute( + select(Answer).filter( + Answer.id.in_( + itertools.chain((aid for aid, _ in sub), global_answer_ids) + ) + ) + ) + .scalars() .all() ) - answs = Answer.query.filter( - Answer.id.in_(itertools.chain((aid for aid, _ in sub), global_answer_ids)) - ).all() answers_with_users: list[tuple[int, Answer | None]] = [] for a in answs: uids = aid_uid_map.get(a.id) @@ -412,13 +433,17 @@ def get_fields_and_users( counts[u.id] = {} cnt = func.count(Answer.id).label("cnt") answer_counts = ( - Answer.query.filter( - Answer.task_id.in_([tid.doc_task for tid in tasks_with_count_field]) + db.session.execute( + select(Answer) + .filter( + Answer.task_id.in_([tid.doc_task for tid in tasks_with_count_field]) + ) + .join(User, Answer.users) + .filter(User.id.in_([u.id for u in users])) + .group_by(User.id, Answer.task_id) + .with_only_columns(User.id, Answer.task_id, cnt) ) - .join(User, Answer.users) - .filter(User.id.in_([u.id for u in users])) - .group_by(User.id, Answer.task_id) - .with_entities(User.id, Answer.task_id, cnt) + .scalars() .all() ) for uid, taskid, count in answer_counts: @@ -545,10 +570,11 @@ def get_tally_field_values( pts = get_points_by_rule( rule=psr, task_ids=tids, - user_ids=User.query.join(UserGroup, join_relation) - .filter(group_filter) - .with_entities(User.id) - .subquery() + user_ids=db.session.execute( + select(User.id).join(UserGroup, join_relation).filter(group_filter) + ) + .scalars() + .all() if group_filter is not None else None, answer_filter=ans_filter, diff --git a/timApp/velp/annotation.py b/timApp/velp/annotation.py index 27bb6aef13..8f1228346b 100644 --- a/timApp/velp/annotation.py +++ b/timApp/velp/annotation.py @@ -10,6 +10,7 @@ from dataclasses import field from flask import Response +from sqlalchemy import select from timApp.answer.answer import Answer from timApp.auth.accesshelper import ( @@ -88,7 +89,7 @@ def add_annotation( require_teacher_if_not_own=True, ) except AccessDenied: - a: Answer = Answer.query.get(answer_id) + a: Answer = db.session.get(Answer, answer_id) if a.has_many_collaborators: raise RouteException( "Reviewing answers with multiple collaborators not supported yet" @@ -224,7 +225,13 @@ def invalidate_annotation(id: int) -> Response: def get_annotation_or_abort(ann_id: int) -> Annotation: # Possibly bug: We need to create a new query object, otherwise raiseload() seems to pollute User's relations - ann = set_annotation_query_opts(db.session.query(Annotation)).get(ann_id) + ann = ( + db.session.execute( + set_annotation_query_opts(select(Annotation).filter_by(id=ann_id)).limit(1) + ) + .scalars() + .first() + ) if not ann: raise RouteException(f"Annotation with id {ann_id} not found") return ann diff --git a/timApp/velp/annotations.py b/timApp/velp/annotations.py index ed44b49bee..d28f59ea95 100644 --- a/timApp/velp/annotations.py +++ b/timApp/velp/annotations.py @@ -9,8 +9,9 @@ from enum import Enum, unique -from sqlalchemy import func, true -from sqlalchemy.orm import joinedload, contains_eager, Query +from sqlalchemy import func, true, select +from sqlalchemy.orm import joinedload, contains_eager +from sqlalchemy.sql import Select from timApp.answer.answer import Answer from timApp.document.docinfo import DocInfo @@ -19,6 +20,7 @@ get_reviews_where_user_is_reviewer_query, is_peerreview_enabled, ) +from timApp.timdb.sqa import db from timApp.user.user import User from timApp.velp.annotation_model import Annotation from timApp.velp.velp_models import VelpContent, VelpVersion, Velp, AnnotationComment @@ -69,39 +71,43 @@ def get_annotations_with_comments_in_document( Annotation.annotator_id == user.id ) anns = ( - set_annotation_query_opts( - Annotation.query.filter_by(document_id=d.id) - .filter( - (Annotation.valid_until == None) - | (Annotation.valid_until >= func.current_timestamp()) + db.session.execute( + set_annotation_query_opts( + select(Annotation) + .filter_by(document_id=d.id) + .filter( + (Annotation.valid_until == None) + | (Annotation.valid_until >= func.current_timestamp()) + ) + .filter((Annotation.annotator_id == user.id) | vis_filter) + .join(VelpVersion) + .join(Velp) + .join(VelpContent) + .filter(VelpContent.language_id == language_id) + .outerjoin(Answer) + .outerjoin(User, Answer.users_all) + .filter(answer_filter) + .filter(own_review_filter) + .order_by( + Annotation.depth_start.desc(), + Annotation.node_start.desc(), + Annotation.offset_start.desc(), + ) ) - .filter((Annotation.annotator_id == user.id) | vis_filter) - .join(VelpVersion) - .join(Velp) - .join(VelpContent) - .filter(VelpContent.language_id == language_id) - .outerjoin(Answer) - .outerjoin(User, Answer.users_all) - .filter(answer_filter) - .filter(own_review_filter) - .order_by( - Annotation.depth_start.desc(), - Annotation.node_start.desc(), - Annotation.offset_start.desc(), + .options(contains_eager(Annotation.velp_content)) + .options(contains_eager(Annotation.answer).contains_eager(Answer.users_all)) + .options( + contains_eager(Annotation.velp_version).contains_eager(VelpVersion.velp) ) + .with_only_columns(Annotation) ) - .options(contains_eager(Annotation.velp_content)) - .options(contains_eager(Annotation.answer).contains_eager(Answer.users_all)) - .options( - contains_eager(Annotation.velp_version).contains_eager(VelpVersion.velp) - ) - .with_entities(Annotation) + .scalars() .all() ) return anns -def set_annotation_query_opts(q: Query) -> Query: +def set_annotation_query_opts(q: Select) -> Select: return ( q.options( joinedload(Annotation.velp_content, innerjoin=True).load_only( diff --git a/timApp/velp/velp.py b/timApp/velp/velp.py index e73ce9a06b..eb07d0936a 100644 --- a/timApp/velp/velp.py +++ b/timApp/velp/velp.py @@ -9,13 +9,9 @@ :version: 1.0.0 """ -from timApp.item.deleting import ( - soft_delete_document, -) from flask import Blueprint, Response - from flask import request -from timApp.user.usergroup import UserGroup +from sqlalchemy import select, delete from timApp.auth.accesshelper import ( verify_logged_in, @@ -26,7 +22,6 @@ AccessDenied, verify_manage_access, ) - from timApp.auth.accesstype import AccessType from timApp.auth.sessioninfo import ( get_current_user_object, @@ -38,11 +33,14 @@ get_documents_in_folder, get_documents, ) - from timApp.document.docinfo import DocInfo from timApp.folder.folder import Folder +from timApp.item.deleting import ( + soft_delete_document, +) from timApp.timdb.sqa import db from timApp.user.user import User +from timApp.user.usergroup import UserGroup from timApp.user.users import get_rights_holders, remove_access from timApp.user.userutils import grant_access from timApp.util.flask.requesthelper import RouteException @@ -171,9 +169,15 @@ def get_default_velp_group(doc_id: int) -> Response: for v in found_velp_groups: # if has_view_access(user_id, timdb.documents.get_document_id(v['name'])): velp_groups.append(v.id) - def_velp_groups: list[VelpGroup] = VelpGroup.query.filter( - VelpGroup.id.in_(velp_groups) & VelpGroup.default_group == True - ).all() + def_velp_groups: list[VelpGroup] = ( + db.session.execute( + select(VelpGroup).filter( + VelpGroup.id.in_(velp_groups) & VelpGroup.default_group == True + ) + ) + .scalars() + .all() + ) if def_velp_groups: default_group = def_velp_groups[0] return no_cache_json_response( @@ -207,16 +211,22 @@ def get_default_personal_velp_group() -> Response: velp_groups = [] for v in found_velp_groups: velp_groups.append(v.id) - default_group = VelpGroup.query.filter( - VelpGroup.id.in_(velp_groups) & VelpGroup.default_group == True - ).first() + default_group = ( + db.session.execute( + select(VelpGroup) + .filter(VelpGroup.id.in_(velp_groups) & VelpGroup.default_group == True) + .limit(1) + ) + .scalars() + .first() + ) if default_group is not None: return no_cache_json_response(default_group) group_name = DEFAULT_PERSONAL_VELP_GROUP_NAME new_group_path = personal_velp_group_path + "/" + group_name group = DocEntry.find_by_path(new_group_path) if group: - vg: VelpGroup = VelpGroup.query.get(group.id) + vg: VelpGroup = db.session.get(VelpGroup, group.id) vg.default_group = True vg.valid_until = None created_new = False @@ -366,7 +376,11 @@ def add_velp() -> Response: # Check where user has edit rights and only add new velp to those velp_groups: list[VelpGroup] = [ vg - for vg in VelpGroup.query.filter(VelpGroup.id.in_(velp_group_ids)).all() + for vg in db.session.execute( + select(VelpGroup).filter(VelpGroup.id.in_(velp_group_ids)) + ) + .scalars() + .all() if has_edit_access(vg.block) ] @@ -434,7 +448,7 @@ def update_velp_route(doc_id: int) -> Response: verify_logged_in() user_id = get_current_user_id() edit_access = False - velp: Velp = Velp.query.get(velp_id) + velp: Velp = db.session.get(Velp, velp_id) if not velp: raise RouteException("Velp not found") all_velp_groups = velp.groups @@ -457,7 +471,11 @@ def update_velp_route(doc_id: int) -> Response: # Check that user has edit access to velp groups in given velp group list and add them to an add list groups_to_add: list[VelpGroup] = [ vg - for vg in VelpGroup.query.filter(VelpGroup.id.in_(velp_group_ids)).all() + for vg in db.session.execute( + select(VelpGroup).filter(VelpGroup.id.in_(velp_group_ids)) + ) + .scalars() + .all() if has_edit_access(vg.block) ] @@ -541,10 +559,18 @@ def update_velp_label_route() -> Response: language_id = json_data.get("language_id") language_id = "FI" if language_id is None else language_id - vlc: VelpLabelContent | None = VelpLabelContent.query.filter_by( - language_id=language_id, - velplabel_id=velp_label_id, - ).first() + vlc: VelpLabelContent | None = ( + db.session.execute( + select(VelpLabelContent) + .filter_by( + language_id=language_id, + velplabel_id=velp_label_id, + ) + .limit(1) + ) + .scalars() + .first() + ) if not vlc: raise RouteException("Velp label not found") u = get_current_user_object() @@ -665,9 +691,13 @@ def reset_target_area_selections_to_defaults(doc_id: int) -> Response: user_id = get_current_user_id() - VelpGroupSelection.query.filter_by( - doc_id=doc_id, user_id=user_id, target_id=target_id - ).delete() + db.session.execute( + delete(VelpGroupSelection).where( + (VelpGroupSelection.doc_id == doc_id) + & (VelpGroupSelection.user_id == user_id) + & (VelpGroupSelection.target_id == target_id) + ) + ) db.session.commit() return ok_response() @@ -680,7 +710,13 @@ def reset_all_selections_to_defaults(doc_id: int) -> Response: """ user_id = get_current_user_id() - VelpGroupSelection.query.filter_by(doc_id=doc_id, user_id=user_id).delete() + + db.session.execute( + delete(VelpGroupSelection).filter_by( + (VelpGroupSelection.doc_id == doc_id) + & (VelpGroupSelection.user_id == user_id) + ) + ) db.session.commit() return ok_response() @@ -881,19 +917,31 @@ def delete_velp_group(group_id: int) -> Response: remove_velp_group_perms(group_id) # Delete associated entries/rows from database - VelpInGroup.query.filter_by(velp_group_id=group_id).delete( - synchronize_session=False + db.session.execute( + delete(VelpInGroup) + .where(VelpInGroup.velp_group_id == group_id) + .execution_options(synchronize_session=False) + ) + db.session.execute( + delete(VelpGroupSelection) + .where(VelpGroupSelection.velp_group_id == group_id) + .execution_options(synchronize_session=False) ) - VelpGroupSelection.query.filter_by(velp_group_id=group_id).delete( - synchronize_session=False + db.session.execute( + delete(VelpGroupDefaults) + .where(VelpGroupDefaults.velp_group_id == group_id) + .execution_options(synchronize_session=False) ) - VelpGroupDefaults.query.filter_by(velp_group_id=group_id).delete( - synchronize_session=False + db.session.execute( + delete(VelpGroupsInDocument) + .where(VelpGroupsInDocument.velp_group_id == group_id) + .execution_options(synchronize_session=False) ) - VelpGroupsInDocument.query.filter_by(velp_group_id=group_id).delete( - synchronize_session=False + db.session.execute( + delete(VelpGroup) + .where(VelpGroup.id == group_id) + .execution_options(synchronize_session=False) ) - VelpGroup.query.filter_by(id=group_id).delete(synchronize_session=False) db.session.commit() return ok_response() @@ -957,7 +1005,7 @@ def get_velp_groups_from_tree(doc: DocInfo) -> list[DocInfo]: # Add found documents to VelpGroup table if they weren't there yet for result in results: - is_velp_group = VelpGroup.query.get(result.id) + is_velp_group = db.session.get(VelpGroup, result.id) if not is_velp_group: _, group_name = split_location(result.path) make_document_a_velp_group(group_name, result.id) diff --git a/timApp/velp/velpgroups.py b/timApp/velp/velpgroups.py index e9c6a7285d..0d7f303757 100644 --- a/timApp/velp/velpgroups.py +++ b/timApp/velp/velpgroups.py @@ -12,6 +12,8 @@ from dataclasses import dataclass, field from typing import Union +from sqlalchemy import select, delete + from timApp.auth.accesstype import AccessType from timApp.document.docentry import DocEntry from timApp.document.docinfo import DocInfo @@ -198,15 +200,23 @@ def get_groups_from_document_table(doc_id: int, user_id: int | None) -> list[Vel """ if not user_id: return ( - VelpGroupsInDocument.query.filter_by(doc_id=doc_id) - .join(VelpGroup) - .with_entities(VelpGroup) + db.session.execute( + select(VelpGroupsInDocument) + .filter_by(doc_id=doc_id) + .join(VelpGroup) + .with_only_columns(VelpGroup) + ) + .scalars() .all() ) return ( - VelpGroupsInDocument.query.filter_by(user_id=user_id, doc_id=doc_id) - .join(VelpGroup) - .with_entities(VelpGroup) + db.session.execute( + select(VelpGroupsInDocument) + .filter_by(user_id=user_id, doc_id=doc_id) + .join(VelpGroup) + .with_only_columns(VelpGroup) + ) + .scalars() .all() ) @@ -226,7 +236,7 @@ def make_document_a_velp_group( :return: velp group ID """ - vg = VelpGroup.query.get(velp_group_id) + vg = db.session.get(VelpGroup, velp_group_id) if vg: return vg vg = VelpGroup( @@ -246,9 +256,13 @@ def add_groups_to_document( velp_groups: list[VelpGroupOrDocInfo], doc: DocInfo, user: User ) -> None: """Adds velp groups to VelpGroupsInDocument table.""" - existing: list[VelpGroupsInDocument] = VelpGroupsInDocument.query.filter_by( - user_id=user.id, doc_id=doc.id - ).all() + existing: list[VelpGroupsInDocument] = ( + db.session.execute( + select(VelpGroupsInDocument).filter_by(user_id=user.id, doc_id=doc.id) + ) + .scalars() + .all() + ) existing_ids = {vgd.velp_group_id for vgd in existing} for velp_group in velp_groups: velp_group_id = velp_group.id @@ -277,12 +291,20 @@ def change_selection( :param selected: Boolean whether group is selected or not """ - vgs: VelpGroupSelection | None = VelpGroupSelection.query.filter_by( - user_id=user_id, - doc_id=doc_id, - velp_group_id=velp_group_id, - target_id=target_id, - ).first() + vgs: VelpGroupSelection | None = ( + db.session.execute( + select(VelpGroupSelection) + .filter_by( + user_id=user_id, + doc_id=doc_id, + velp_group_id=velp_group_id, + target_id=target_id, + ) + .limit(1) + ) + .scalars() + .first() + ) if vgs: vgs.target_type = target_type vgs.selected = selected @@ -310,12 +332,20 @@ def change_all_target_area_default_selections( :param selected: True or False """ - VelpGroupDefaults.query.filter_by( - doc_id=doc_id, target_type=target_type, target_id=target_id - ).delete() - vgids: list[VelpGroupsInDocument] = VelpGroupsInDocument.query.filter_by( - doc_id=doc_id, user_id=user_id - ).all() + db.session.execute( + delete(VelpGroupDefaults).where( + (VelpGroupDefaults.doc_id == doc_id) + & (VelpGroupDefaults.target_type == target_type) + & (VelpGroupDefaults.target_id == target_id) + ) + ) + vgids: list[VelpGroupsInDocument] = ( + db.session.execute( + select(VelpGroupsInDocument).filter_by(doc_id=doc_id, user_id=user_id) + ) + .scalars() + .all() + ) for vgid in vgids: vgd = VelpGroupDefaults( doc_id=doc_id, @@ -340,21 +370,38 @@ def change_all_target_area_selections( """ if target_type == 0: - for vgs in VelpGroupSelection.query.filter_by( - doc_id=doc_id, target_id=target_id, user_id=user_id - ).all(): + for vgs in ( + db.session.execute( + select(VelpGroupSelection).filter_by( + doc_id=doc_id, target_id=target_id, user_id=user_id + ) + ) + .scalars() + .all() + ): vgs.selected = selected elif target_type == 1: - VelpGroupSelection.query.filter_by( - doc_id=doc_id, target_id=target_id, user_id=user_id, target_type=target_type - ).delete() + db.session.execute( + delete(VelpGroupSelection).where( + (VelpGroupSelection.doc_id == doc_id) + & (VelpGroupSelection.target_id == target_id) + & (VelpGroupSelection.user_id == user_id) + & (VelpGroupSelection.target_type == target_type) + ) + ) # target_type is 0 because only 0 always contains all velp groups user has access to. # Other target types will get added to database only after they've been clicked once in interface. - vgss: list[VelpGroupSelection] = VelpGroupSelection.query.filter_by( - doc_id=doc_id, - user_id=user_id, - target_type=0, - ).all() + vgss: list[VelpGroupSelection] = ( + db.session.execute( + select(VelpGroupSelection).filter_by( + doc_id=doc_id, + user_id=user_id, + target_type=0, + ) + ) + .scalars() + .all() + ) for vgs in vgss: nvgs = VelpGroupSelection( user_id=user_id, @@ -379,11 +426,19 @@ def change_default_selection( :param selected: Boolean whether group is selected or not """ - vgd: VelpGroupDefaults = VelpGroupDefaults.query.filter_by( - doc_id=doc_id, - velp_group_id=velp_group_id, - target_id=target_id, - ).first() + vgd: VelpGroupDefaults = ( + db.session.execute( + select(VelpGroupDefaults) + .filter_by( + doc_id=doc_id, + velp_group_id=velp_group_id, + target_id=target_id, + ) + .limit(1) + ) + .scalars() + .first() + ) if vgd: vgd.selected = selected vgd.target_type = target_type @@ -406,12 +461,20 @@ def add_groups_to_selection_table( target_id: str, ) -> None: """Adds velp groups to VelpGroupSelection table.""" - vgs = VelpGroupSelection.query.filter_by( - user_id=user_id, - doc_id=doc_id, - velp_group_id=velp_group.id, - target_id=target_id, - ).first() + vgs = ( + db.session.execute( + select(VelpGroupSelection) + .filter_by( + user_id=user_id, + doc_id=doc_id, + velp_group_id=velp_group.id, + target_id=target_id, + ) + .limit(1) + ) + .scalars() + .first() + ) if vgs: vgs.selected = True vgs.target_type = target_type @@ -430,10 +493,8 @@ def add_groups_to_selection_table( def process_selection_info( vgss: list[VelpGroupSelection] | list[VelpGroupDefaults], ) -> VelpGroupSelectionInfo: - groups = VelpGroupSelectionInfo() if vgss: - target_id: str = "0" i: int = 0 while i < len(vgss): @@ -460,8 +521,12 @@ def get_personal_selections_for_velp_groups( """ vgss = ( - VelpGroupSelection.query.filter_by(doc_id=doc_id, user_id=user_id) - .order_by(VelpGroupSelection.target_id) + db.session.execute( + select(VelpGroupSelection) + .filter_by(doc_id=doc_id, user_id=user_id) + .order_by(VelpGroupSelection.target_id) + ) + .scalars() .all() ) return process_selection_info(vgss) @@ -477,8 +542,12 @@ def get_default_selections_for_velp_groups( """ vgds = ( - VelpGroupDefaults.query.filter_by(doc_id=doc_id) - .order_by(VelpGroupDefaults.target_id) + db.session.execute( + select(VelpGroupDefaults) + .filter_by(doc_id=doc_id) + .order_by(VelpGroupDefaults.target_id) + ) + .scalars() .all() ) return process_selection_info(vgds) diff --git a/timApp/velp/velps.py b/timApp/velp/velps.py index f521fa2074..c5ee0ea4ff 100644 --- a/timApp/velp/velps.py +++ b/timApp/velp/velps.py @@ -7,9 +7,9 @@ """ -from typing import Optional, Iterable +from typing import Iterable -from sqlalchemy import func +from sqlalchemy import func, delete, select from sqlalchemy.orm import joinedload from timApp.timdb.sqa import db @@ -152,7 +152,7 @@ def update_velp( """ if not visible_to: visible_to = 4 - v: Velp = Velp.query.get(velp_id) + v: Velp = db.session.get(Velp, velp_id) if v: v.default_points = default_points v.color = color @@ -183,7 +183,7 @@ def update_velp_labels(velp_id: int, labels: Iterable[int]) -> None: """ # First nuke existing labels. - LabelInVelp.query.filter_by(velp_id=velp_id).delete() + db.session.execute(delete(LabelInVelp).where(LabelInVelp.velp_id == velp_id)) # Then add the new ones. add_labels_to_velp(velp_id, labels) @@ -199,11 +199,16 @@ def get_latest_velp_version( """ return ( - VelpContent.query.filter_by(language_id=language_id) - .join(VelpVersion) - .filter_by(velp_id=velp_id) - .order_by(VelpVersion.id.desc()) - .with_entities(VelpContent) + db.session.execute( + select(VelpContent) + .filter_by(language_id=language_id) + .join(VelpVersion) + .filter_by(velp_id=velp_id) + .order_by(VelpVersion.id.desc()) + .with_only_columns(VelpContent) + .limit(1) + ) + .scalars() .first() ) @@ -222,12 +227,14 @@ def get_velp_content_for_document( """ sq = ( - VelpVersion.query.group_by(VelpVersion.velp_id) - .with_entities(VelpVersion.velp_id, func.max(VelpVersion.id).label("ver")) + select(VelpVersion) + .group_by(VelpVersion.velp_id) + .with_only_columns(VelpVersion.velp_id, func.max(VelpVersion.id).label("ver")) .subquery() ) vq = ( - Velp.query.join(sq, sq.c.velp_id == Velp.id) + select(Velp) + .join(sq, sq.c.velp_id == Velp.id) .join(VelpContent, sq.c.ver == VelpContent.version_id) .filter(VelpContent.language_id == language_id) .filter( @@ -243,7 +250,7 @@ def get_velp_content_for_document( (VelpGroupsInDocument.user_id == user_id) & (VelpGroupsInDocument.doc_id == doc_id) ) - .with_entities(Velp) + .with_only_columns(Velp) .options(joinedload(Velp.groups, innerjoin=True).raiseload(VelpGroup.block)) .options( joinedload(Velp.velp_versions, innerjoin=True).joinedload( @@ -269,19 +276,24 @@ def get_velp_label_content_for_document( """ vlcs = ( - VelpLabelContent.query.filter_by(language_id=language_id) - .join(LabelInVelp, VelpLabelContent.velplabel_id == LabelInVelp.label_id) - .join(Velp) - .filter( - (Velp.valid_until >= func.current_timestamp()) | (Velp.valid_until == None) - ) - .join(VelpInGroup) - .join( - VelpGroupsInDocument, - VelpInGroup.velp_group_id == VelpGroupsInDocument.velp_group_id, + db.session.execute( + select(VelpLabelContent) + .filter_by(language_id=language_id) + .join(LabelInVelp, VelpLabelContent.velplabel_id == LabelInVelp.label_id) + .join(Velp) + .filter( + (Velp.valid_until >= func.current_timestamp()) + | (Velp.valid_until == None) + ) + .join(VelpInGroup) + .join( + VelpGroupsInDocument, + VelpInGroup.velp_group_id == VelpGroupsInDocument.velp_group_id, + ) + .filter_by(doc_id=doc_id, user_id=user_id) + .with_only_columns(VelpLabelContent) ) - .filter_by(doc_id=doc_id, user_id=user_id) - .with_entities(VelpLabelContent) + .scalars() .all() ) return vlcs From aaf30be151ddd54173633a4e54eb7e618feecabf Mon Sep 17 00:00:00 2001 From: dezhidki Date: Wed, 19 Jul 2023 15:14:20 +0300 Subject: [PATCH 04/34] Refactor to use selectin relationship loader by default Based on the recommendations of SQLAlchemy, selectin loading should be preferred over joined loading for all relationships where there is a chance of database duplicating the rows. --- timApp/admin/answer_cli.py | 14 +++-- timApp/answer/answers.py | 4 +- timApp/answer/routes.py | 5 +- timApp/auth/sessioninfo.py | 4 +- timApp/document/course/routes.py | 5 +- timApp/document/docentry.py | 2 +- timApp/document/docinfo.py | 6 +- timApp/folder/folder.py | 63 ++++++++++++++++----- timApp/item/block.py | 2 +- timApp/item/item.py | 6 +- timApp/item/routes.py | 12 ++-- timApp/item/routes_tags.py | 6 +- timApp/item/taskblock.py | 2 +- timApp/lecture/askedjson.py | 2 +- timApp/lecture/askedquestion.py | 4 +- timApp/lecture/lectureanswer.py | 4 +- timApp/lecture/routes.py | 8 +-- timApp/note/routes.py | 8 +-- timApp/notification/notify.py | 8 +-- timApp/notification/pending_notification.py | 2 +- timApp/peerreview/util/peerreview_utils.py | 6 +- timApp/plugin/tableform/tableForm.py | 4 +- timApp/readmark/readings.py | 8 ++- timApp/scheduling/scheduling_routes.py | 8 +-- timApp/sisu/sisu.py | 8 +-- timApp/user/personaluniquecode.py | 4 +- timApp/user/user.py | 12 ++-- timApp/user/usergroup.py | 6 +- timApp/user/users.py | 10 ++-- timApp/util/flask/search.py | 6 +- timApp/util/get_fields.py | 4 +- timApp/velp/annotations.py | 24 +++----- timApp/velp/velp_models.py | 2 +- timApp/velp/velps.py | 10 +--- 34 files changed, 156 insertions(+), 123 deletions(-) diff --git a/timApp/admin/answer_cli.py b/timApp/admin/answer_cli.py index ce5401fccf..eaeb72c00a 100644 --- a/timApp/admin/answer_cli.py +++ b/timApp/admin/answer_cli.py @@ -7,7 +7,7 @@ import click from flask.cli import AppGroup from sqlalchemy import func, select, delete -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from timApp.admin.datetimetype import DateTimeType from timApp.admin.timitemtype import TimDocumentType, TimItemType @@ -225,11 +225,15 @@ def truncate_large(doc: DocInfo, limit: int, to: int, dry_run: bool) -> None: sys.exit(1) stmt = select(Answer).filter(Answer.task_id.startswith(f"{doc.id}.")) total = db.session.scalar(stmt.with_only_columns([func.count()])) - anss: list[Answer] = db.session.scalars( - stmt.filter(func.length(Answer.content) > limit).options( - joinedload(Answer.users_all) + anss: list[Answer] = ( + db.session.execute( + stmt.filter(func.length(Answer.content) > limit).options( + selectinload(Answer.users_all) + ) ) - ).all() + .scalars() + .all() + ) note = " (answer truncated)" try_keys = ["usercode", "c", "userinput"] truncated = 0 diff --git a/timApp/answer/answers.py b/timApp/answer/answers.py index d2daacc508..164b6a2e6f 100644 --- a/timApp/answer/answers.py +++ b/timApp/answer/answers.py @@ -22,7 +22,7 @@ from flask import current_app from sqlalchemy import func, Numeric, Float, true, case, select from sqlalchemy.dialects.postgresql import aggregate_order_by -from sqlalchemy.orm import selectinload, defaultload, joinedload, contains_eager +from sqlalchemy.orm import defaultload, selectinload, contains_eager from sqlalchemy.sql import Select, Subquery from timApp.answer.answer import Answer @@ -654,7 +654,7 @@ def get_users_for_tasks( if user_ids is not None: main_stmt = main_stmt.filter(User.id.in_(user_ids)) if current_app.config["LOAD_STUDENT_IDS_IN_TEACHER"]: - main_stmt = main_stmt.options(joinedload("uniquecodes")) + main_stmt = main_stmt.options(selectinload("uniquecodes")) main_stmt = main_stmt.group_by(User.id, *group_by_cols) # prevents error: diff --git a/timApp/answer/routes.py b/timApp/answer/routes.py index 37ddd39667..1c53268f58 100644 --- a/timApp/answer/routes.py +++ b/timApp/answer/routes.py @@ -11,7 +11,7 @@ from flask import request from marshmallow.utils import missing from sqlalchemy import func, select -from sqlalchemy.orm import lazyload, joinedload, selectinload +from sqlalchemy.orm import lazyload, selectinload from werkzeug.exceptions import NotFound from timApp.answer.answer import Answer, AnswerData @@ -173,6 +173,7 @@ None, # Clear points, only by teacher ] + # TODO: loggable route (points in url?) @answers.put("/saveReview//") def save_review_points( @@ -1792,7 +1793,7 @@ def get_answers(task_id: str, user_id: int) -> Response: select(Answer) .filter_by(task_id=tid.doc_task) .order_by(Answer.id.desc()) - .options(joinedload(Answer.users_all)) + .options(selectinload(Answer.users_all)) ).all() user = curr_user else: diff --git a/timApp/auth/sessioninfo.py b/timApp/auth/sessioninfo.py index 4ad4d840c6..2d9c6b7728 100644 --- a/timApp/auth/sessioninfo.py +++ b/timApp/auth/sessioninfo.py @@ -2,7 +2,7 @@ from flask import session, g, request, has_request_context from sqlalchemy import select -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from timApp.document.usercontext import UserContext from timApp.timdb.sqa import db @@ -27,7 +27,7 @@ def get_current_user_object() -> User: u: User | None = db.session.get( User, curr_id, - options=[joinedload(User.lectures), joinedload(User.groups)], + options=[selectinload(User.lectures), selectinload(User.groups)], ) if u is None: if curr_id != 0: diff --git a/timApp/document/course/routes.py b/timApp/document/course/routes.py index c3dbcebab6..6af71e4706 100644 --- a/timApp/document/course/routes.py +++ b/timApp/document/course/routes.py @@ -3,10 +3,9 @@ """ from flask import Blueprint, current_app, Response -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from timApp.auth.sessioninfo import get_current_user_object -from timApp.bookmark.bookmarks import Bookmarks from timApp.document.docentry import DocEntry, get_documents from timApp.item.block import Block from timApp.util.flask.responsehelper import json_response @@ -50,6 +49,6 @@ def get_documents_from_bookmark_folder(foldername: str) -> Response: docs = get_documents( filter_user=get_current_user_object(), custom_filter=DocEntry.name.in_(paths), - query_options=joinedload(DocEntry._block).joinedload(Block.tags), + query_options=selectinload(DocEntry._block).selectinload(Block.tags), ) return json_response(docs) diff --git a/timApp/document/docentry.py b/timApp/document/docentry.py index 91d8bc6ba0..757cf20ced 100644 --- a/timApp/document/docentry.py +++ b/timApp/document/docentry.py @@ -41,7 +41,7 @@ class DocEntry(db.Model, DocInfo): public = db.Column(db.Boolean, nullable=False, default=True) """Whether the document is visible in directory listing.""" - _block = db.relationship("Block", back_populates="docentries", lazy="joined") + _block = db.relationship("Block", back_populates="docentries", lazy="selectin") trs: list[Translation] = db.relationship( "Translation", diff --git a/timApp/document/docinfo.py b/timApp/document/docinfo.py index 8b1296b73a..7ddc3ed2a4 100644 --- a/timApp/document/docinfo.py +++ b/timApp/document/docinfo.py @@ -4,7 +4,7 @@ from typing import Iterable, Generator, TYPE_CHECKING from sqlalchemy import select -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from timApp.document.docparagraph import DocParagraph from timApp.document.document import Document @@ -184,7 +184,7 @@ def get_docs(doc_paths: list[str]) -> list[tuple[DocEntry, Translation | None]]: (Translation.src_docid == DocEntry.id) & (Translation.lang_id == self.lang_id), ) - ).scalars().all() + ).all() result = get_docs(paths) result.sort(key=lambda x: path_index_map[x[0].path]) @@ -246,7 +246,7 @@ def get_notifications(self, condition) -> list[Notification]: stmt = ( select(Notification) - .options(joinedload(Notification.user).joinedload(User.groups)) + .options(selectinload(Notification.user).selectinload(User.groups)) .filter(Notification.block_id.in_([f.id for f in items])) ) stmt = stmt.filter(condition) diff --git a/timApp/folder/folder.py b/timApp/folder/folder.py index 2b44a8ac30..a7a20cc62c 100644 --- a/timApp/folder/folder.py +++ b/timApp/folder/folder.py @@ -38,7 +38,7 @@ class Folder(db.Model, Item): __table_args__ = (db.UniqueConstraint("name", "location", name="folder_uc"),) - _block = db.relationship("Block", back_populates="folder", lazy="joined") + _block = db.relationship("Block", back_populates="folder", lazy="selectin") @staticmethod def get_root() -> Folder: @@ -46,11 +46,17 @@ def get_root() -> Folder: @staticmethod def get_by_id(fid) -> Folder | None: - return db.session.get(Folder, fid) if fid != ROOT_FOLDER_ID else Folder.get_root() + return ( + db.session.get(Folder, fid) if fid != ROOT_FOLDER_ID else Folder.get_root() + ) @staticmethod def find_by_location(location, name) -> Folder | None: - return db.session.execute(select(Folder).filter_by(name=name, location=location)).scalars().first() + return ( + db.session.execute(select(Folder).filter_by(name=name, location=location)) + .scalars() + .first() + ) @staticmethod def find_by_path(path, fallback_to_id=False) -> Folder | None: @@ -122,8 +128,12 @@ def is_root(self) -> bool: def delete(self): assert self.is_empty db.session.delete(self) - db.session.execute(delete(BlockAccess).where(BlockAccess.block_id==self.id)) - db.session.execute(delete(Block).where((Block.type_id==BlockType.Folder.value) & (Block.id==self.id))) + db.session.execute(delete(BlockAccess).where(BlockAccess.block_id == self.id)) + db.session.execute( + delete(Block).where( + (Block.type_id == BlockType.Folder.value) & (Block.id == self.id) + ) + ) def rename(self, new_name: str): assert "/" not in new_name @@ -144,15 +154,26 @@ def rename_path(self, new_path: str) -> None: def rename_content(self, old_path: str, new_path: str): """Renames contents of the folder.""" - docs_in_folder: list[DocEntry] = db.session.execute(select(DocEntry).filter( - DocEntry.name.like(old_path + "/%") - )).scalars().all() + docs_in_folder: list[DocEntry] = ( + db.session.execute( + select(DocEntry).filter(DocEntry.name.like(old_path + "/%")) + ) + .scalars() + .all() + ) for d in docs_in_folder: d.name = d.name.replace(old_path, new_path, 1) - folders_in_folder = db.session.execute(select(Folder).filter( - (Folder.location == old_path) | (Folder.location.like(old_path + "/%")) - )).scalars().all() + folders_in_folder = ( + db.session.execute( + select(Folder).filter( + (Folder.location == old_path) + | (Folder.location.like(old_path + "/%")) + ) + ) + .scalars() + .all() + ) for f in folders_in_folder: f.location = f.location.replace(old_path, new_path, 1) @@ -202,9 +223,15 @@ def get_full_path(self) -> str: def get_document( self, relative_path: str, create_if_not_exist=False, creator_group=None ) -> None | DocEntry: - doc = db.session.execute(select(DocEntry).filter_by( - name=join_location(self.get_full_path(), relative_path) - )).scalars().first() + doc = ( + db.session.execute( + select(DocEntry).filter_by( + name=join_location(self.get_full_path(), relative_path) + ) + ) + .scalars() + .first() + ) if doc is not None: return doc if create_if_not_exist: @@ -283,7 +310,13 @@ def create( return Folder.get_root() rel_path, rel_name = split_location(path) - folder = db.session.execute(select(Folder).filter_by(name=rel_name, location=rel_path)).scalars().first() + folder = ( + db.session.execute( + select(Folder).filter_by(name=rel_name, location=rel_path) + ) + .scalars() + .first() + ) if folder is not None: return folder diff --git a/timApp/item/block.py b/timApp/item/block.py index fcb85199dd..e1a16ff0cb 100644 --- a/timApp/item/block.py +++ b/timApp/item/block.py @@ -62,7 +62,7 @@ class Block(db.Model): accesses = db.relationship( "BlockAccess", back_populates="block", - lazy="joined", + lazy="selectin", cascade="all, delete-orphan", collection_class=attribute_mapped_collection("block_collection_key"), ) diff --git a/timApp/item/item.py b/timApp/item/item.py index 509ef8eec5..a13c053b93 100644 --- a/timApp/item/item.py +++ b/timApp/item/item.py @@ -125,13 +125,13 @@ def parents_to_root(self, include_root=True, eager_load_groups=False): select(Folder) .filter(tuple_(Folder.location, Folder.name).in_(path_tuples)) .order_by(func.length(Folder.location).desc()) - .options(defaultload(Folder._block).joinedload(Block.relevance)) + .options(defaultload(Folder._block).selectinload(Block.relevance)) ) if eager_load_groups: crumbs_stmt = crumbs_stmt.options( defaultload(Folder._block) - .joinedload(Block.accesses) - .joinedload(BlockAccess.usergroup) + .selectinload(Block.accesses) + .selectinload(BlockAccess.usergroup) ) crumbs = db.session.execute(crumbs_stmt).scalars().all() if include_root: diff --git a/timApp/item/routes.py b/timApp/item/routes.py index 1375502486..25b00422a8 100644 --- a/timApp/item/routes.py +++ b/timApp/item/routes.py @@ -17,7 +17,7 @@ from markupsafe import Markup from marshmallow import EXCLUDE from sqlalchemy import select -from sqlalchemy.orm import joinedload, defaultload +from sqlalchemy.orm import selectinload, defaultload from timApp.answer.answers import add_missing_users_from_groups, get_points_by_rule from timApp.auth.accesshelper import ( @@ -525,12 +525,12 @@ def view(item_path: str, route: ViewRoute, render_doc: bool = True) -> FlaskView docentry_load_opts=( defaultload(DocEntry._block) .defaultload(Block.accesses) - .joinedload(BlockAccess.usergroup), - joinedload(DocEntry.trs) - # TODO: These joinedloads are for some reason very inefficient at least for certain documents. + .selectinload(BlockAccess.usergroup), + selectinload(DocEntry.trs) + # TODO: These selectinloads are for some reason very inefficient at least for certain documents. # See https://github.com/TIM-JYU/TIM/issues/2201. Needs more investigation. - # .joinedload(Translation.docentry), - # joinedload(DocEntry.trs).joinedload(Translation._block) + # .selectinload(Translation.docentry), + # selectinload(DocEntry.trs).selectinload(Translation._block) ), ) if doc_info is None: diff --git a/timApp/item/routes_tags.py b/timApp/item/routes_tags.py index 434347886f..e5df31847e 100644 --- a/timApp/item/routes_tags.py +++ b/timApp/item/routes_tags.py @@ -7,7 +7,7 @@ from flask import request, Response from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from sqlalchemy.orm.exc import UnmappedInstanceError, FlushError # type: ignore from timApp.auth.accesshelper import ( @@ -281,7 +281,7 @@ def get_tagged_documents() -> Response: ) if list_doc_tags: - query_options = joinedload(DocEntry._block).joinedload(Block.tags) + query_options = selectinload(DocEntry._block).selectinload(Block.tags) else: query_options = None @@ -303,7 +303,7 @@ def get_tagged_document_by_id(doc_id: int) -> Response: docs = get_documents( filter_user=get_current_user_object(), custom_filter=DocEntry.id.in_([doc_id]), - query_options=joinedload(DocEntry._block).joinedload(Block.tags), + query_options=selectinload(DocEntry._block).selectinload(Block.tags), ) if not docs: raise NotExist("Document not found or not accessible!") diff --git a/timApp/item/taskblock.py b/timApp/item/taskblock.py index 40cf45e8a2..07a8b9796f 100644 --- a/timApp/item/taskblock.py +++ b/timApp/item/taskblock.py @@ -12,7 +12,7 @@ class TaskBlock(db.Model): id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) task_id = db.Column(db.Text, primary_key=True) - block = db.relationship("Block", lazy="joined") + block = db.relationship("Block", lazy="selectin") @staticmethod def get_by_task(task_id: str) -> TaskBlock | None: diff --git a/timApp/lecture/askedjson.py b/timApp/lecture/askedjson.py index 71d85de3ef..2e1bb1c105 100644 --- a/timApp/lecture/askedjson.py +++ b/timApp/lecture/askedjson.py @@ -14,7 +14,7 @@ class AskedJson(db.Model): hash = db.Column(db.Text, nullable=False) asked_questions = db.relationship( - "AskedQuestion", back_populates="asked_json", lazy="joined" + "AskedQuestion", back_populates="asked_json", lazy="selectin" ) def to_json(self, hide_points=False): diff --git a/timApp/lecture/askedquestion.py b/timApp/lecture/askedquestion.py index b972002ecc..a5936c050a 100644 --- a/timApp/lecture/askedquestion.py +++ b/timApp/lecture/askedquestion.py @@ -29,10 +29,10 @@ class AskedQuestion(db.Model): expl = db.Column(db.Text) asked_json: AskedJson = db.relationship( - "AskedJson", back_populates="asked_questions", lazy="joined" + "AskedJson", back_populates="asked_questions", lazy="selectin" ) lecture: Lecture = db.relationship( - "Lecture", back_populates="asked_questions", lazy="joined" + "Lecture", back_populates="asked_questions", lazy="selectin" ) answers = db.relationship( "LectureAnswer", back_populates="asked_question", lazy="dynamic" diff --git a/timApp/lecture/lectureanswer.py b/timApp/lecture/lectureanswer.py index 1808bfd298..093f9d3ff5 100644 --- a/timApp/lecture/lectureanswer.py +++ b/timApp/lecture/lectureanswer.py @@ -39,9 +39,9 @@ class LectureAnswer(db.Model): points = db.Column(db.Float) asked_question = db.relationship( - "AskedQuestion", back_populates="answers", lazy="joined" + "AskedQuestion", back_populates="answers", lazy="selectin" ) - user = db.relationship("User", back_populates="lectureanswers", lazy="joined") + user = db.relationship("User", back_populates="lectureanswers", lazy="selectin") @staticmethod def get_by_id(ans_id: int) -> Optional["LectureAnswer"]: diff --git a/timApp/lecture/routes.py b/timApp/lecture/routes.py index 92d28e2f10..0c39045f00 100644 --- a/timApp/lecture/routes.py +++ b/timApp/lecture/routes.py @@ -12,7 +12,7 @@ from flask import session from sqlalchemy import func, select, delete from sqlalchemy.exc import OperationalError -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from sqlalchemy.orm.exc import StaleDataError from timApp.auth.accesshelper import ( @@ -89,13 +89,13 @@ def get_lecture_info(): u = get_current_user_object() lecture_questions: list[AskedQuestion] = ( lecture.asked_questions.options( - joinedload(AskedQuestion.answers_all).raiseload( + selectinload(AskedQuestion.answers_all).raiseload( LectureAnswer.asked_question ) ) .options( - joinedload(AskedQuestion.answers_all) - .joinedload(LectureAnswer.user) + selectinload(AskedQuestion.answers_all) + .selectinload(LectureAnswer.user) .raiseload(User.groups) ) .all() diff --git a/timApp/note/routes.py b/timApp/note/routes.py index 0f67c93e0f..1f79a51e73 100644 --- a/timApp/note/routes.py +++ b/timApp/note/routes.py @@ -4,7 +4,7 @@ from flask import Response from sqlalchemy import true, select -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from timApp.auth.accesshelper import ( verify_comment_right, @@ -146,8 +146,8 @@ def get_notes( db.session.execute( select(UserNote) .filter(UserNote.doc_id.in_(d_ids) & access_restriction & time_restriction) - .options(joinedload(UserNote.usergroup)) - .options(joinedload(UserNote.block).joinedload(Block.docentries)) + .options(selectinload(UserNote.usergroup)) + .options(selectinload(UserNote.block).selectinload(Block.docentries)) ) .scalars() .all() @@ -166,7 +166,7 @@ def get_notes( & (PendingNotification.kind == NotificationType.CommentDeleted) ) .options( - joinedload(PendingNotification.block).joinedload( + selectinload(PendingNotification.block).selectinload( Block.docentries ) ) diff --git a/timApp/notification/notify.py b/timApp/notification/notify.py index e79f5030dd..6d01c14a58 100644 --- a/timApp/notification/notify.py +++ b/timApp/notification/notify.py @@ -6,7 +6,7 @@ from flask import current_app from sqlalchemy import select -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from timApp.auth.accesshelper import ( verify_logged_in, @@ -85,9 +85,9 @@ def get_current_user_notifications(limit: int | None = None): stmt = ( select(Notification) .filter_by(user_id=get_current_user_id()) - .options(joinedload(Notification.block).joinedload(Block.docentries)) - .options(joinedload(Notification.block).joinedload(Block.folder)) - .options(joinedload(Notification.block).joinedload(Block.translation)) + .options(selectinload(Notification.block).selectinload(Block.docentries)) + .options(selectinload(Notification.block).selectinload(Block.folder)) + .options(selectinload(Notification.block).selectinload(Block.translation)) .order_by(Notification.block_id.desc()) ) diff --git a/timApp/notification/pending_notification.py b/timApp/notification/pending_notification.py index b25be4079d..a3402dbfe3 100644 --- a/timApp/notification/pending_notification.py +++ b/timApp/notification/pending_notification.py @@ -21,7 +21,7 @@ class PendingNotification(db.Model): processed = db.Column(db.DateTime(timezone=True), nullable=True, index=True) kind = db.Column(db.Enum(NotificationType), nullable=False) - user: User = db.relationship("User", lazy="joined") + user: User = db.relationship("User", lazy="selectin") block = db.relationship("Block") @property diff --git a/timApp/peerreview/util/peerreview_utils.py b/timApp/peerreview/util/peerreview_utils.py index acf383414d..04d5173e47 100644 --- a/timApp/peerreview/util/peerreview_utils.py +++ b/timApp/peerreview/util/peerreview_utils.py @@ -8,7 +8,7 @@ from sqlalchemy import select from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from sqlalchemy.sql import Select from timApp.document.docinfo import DocInfo @@ -23,7 +23,7 @@ def get_reviews_where_user_is_reviewer(d: DocInfo, user: User) -> list[PeerReview]: """Return all peer_review rows where block_is is d.id and the person making the review is the given user""" stmt = get_reviews_where_user_is_reviewer_query(d, user).options( - joinedload(PeerReview.reviewable) + selectinload(PeerReview.reviewable) ) return db.session.execute(stmt).scalars().all() @@ -43,7 +43,7 @@ def get_all_reviews(doc: DocInfo) -> list[PeerReview]: def get_reviews_targeting_user(d: DocInfo, user: User) -> list[PeerReview]: """Return all peer_review rows where block_id is d.id and the user is the review target""" stmt = get_reviews_targeting_user_query(d, user).options( - joinedload(PeerReview.reviewable) + selectinload(PeerReview.reviewable) ) return db.session.execute(stmt).scalars().all() diff --git a/timApp/plugin/tableform/tableForm.py b/timApp/plugin/tableform/tableForm.py index 03233c700b..4f19b07e3b 100644 --- a/timApp/plugin/tableform/tableForm.py +++ b/timApp/plugin/tableform/tableForm.py @@ -9,7 +9,7 @@ from flask import render_template_string, Response, send_file from marshmallow.utils import missing from sqlalchemy import select -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from webargs.flaskparser import use_args from timApp.auth.accesshelper import get_doc_or_abort, AccessDenied @@ -187,7 +187,7 @@ def get_sisugroups(user: User, sisu_id: str | None) -> "TableFormObj": Tag.name.in_([GROUP_TAG_PREFIX + g.name for g in gs]) & Tag.block_id.in_(docs_with_course_tag) ) - .options(joinedload(Tag.block).joinedload(Block.docentries)) + .options(selectinload(Tag.block).selectinload(Block.docentries)) ) .scalars() .all() diff --git a/timApp/readmark/readings.py b/timApp/readmark/readings.py index 04124f81af..f2338748ff 100644 --- a/timApp/readmark/readings.py +++ b/timApp/readmark/readings.py @@ -22,7 +22,13 @@ def get_read_expiry_condition(delta: timedelta): def get_readings( usergroup_id: int, doc: Document, filter_condition=None ) -> list[ReadParagraph]: - return get_readings_filtered_query(usergroup_id, doc, filter_condition).all() + return ( + db.session.execute( + get_readings_filtered_query(usergroup_id, doc, filter_condition) + ) + .scalars() + .all() + ) def has_anything_read(usergroup_ids: list[int], doc: Document) -> bool: diff --git a/timApp/scheduling/scheduling_routes.py b/timApp/scheduling/scheduling_routes.py index b4c670af62..262d3f5511 100644 --- a/timApp/scheduling/scheduling_routes.py +++ b/timApp/scheduling/scheduling_routes.py @@ -5,7 +5,7 @@ from flask import current_app, Response from isodate import Duration from sqlalchemy import select -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from timApp.auth.accesshelper import ( get_doc_or_abort, @@ -69,9 +69,9 @@ def get_scheduled_functions(all_users: bool = False) -> Response: .filter(BlockAccess.type == AccessType.owner.value) .join(PeriodicTask) .options( - joinedload(PeriodicTask.block) - .joinedload(Block.accesses) - .joinedload(BlockAccess.usergroup) + selectinload(PeriodicTask.block) + .selectinload(Block.accesses) + .selectinload(BlockAccess.usergroup) ) ) diff --git a/timApp/sisu/sisu.py b/timApp/sisu/sisu.py index 84e92f03e4..18f2f50dd9 100644 --- a/timApp/sisu/sisu.py +++ b/timApp/sisu/sisu.py @@ -10,7 +10,7 @@ from marshmallow.utils import _Missing, missing from sqlalchemy import any_, true, select from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from webargs.flaskparser import use_args from timApp.auth.accesshelper import get_doc_or_abort, AccessDenied @@ -281,9 +281,9 @@ def refresh_sisu_grouplist_doc(ug: UserGroup) -> None: # Update rights for already existing activated groups. docs = d.parent.get_all_documents( - query_options=joinedload(DocEntry._block) - .joinedload(Block.managed_usergroup) - .joinedload(UserGroup.external_id), + query_options=selectinload(DocEntry._block) + .selectinload(Block.managed_usergroup) + .selectinload(UserGroup.external_id), ) for doc in docs: if doc == d: diff --git a/timApp/user/personaluniquecode.py b/timApp/user/personaluniquecode.py index ad10dec522..a9c15e3943 100644 --- a/timApp/user/personaluniquecode.py +++ b/timApp/user/personaluniquecode.py @@ -30,9 +30,9 @@ class PersonalUniqueCode(db.Model): type = db.Column(db.Text, nullable=False, primary_key=True) """The type of the code, e.g. student or employee.""" - user = db.relationship("User", back_populates="uniquecodes", lazy="joined") + user = db.relationship("User", back_populates="uniquecodes", lazy="selectin") organization = db.relationship( - "HakaOrganization", back_populates="uniquecodes", lazy="joined" + "HakaOrganization", back_populates="uniquecodes", lazy="selectin" ) __table_args__ = (db.UniqueConstraint("org_id", "code", "type"),) diff --git a/timApp/user/user.py b/timApp/user/user.py index e500dc6293..c34cc40763 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -9,7 +9,7 @@ from flask import current_app, has_request_context from sqlalchemy import func, select, delete from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import joinedload, defaultload +from sqlalchemy.orm import selectinload, defaultload from sqlalchemy.orm.collections import ( attribute_mapped_collection, ) @@ -236,7 +236,7 @@ def last_name_to_last(full_name: str | None): def user_query_with_joined_groups() -> Select: - return select(User).options(joinedload(User.groups)) + return select(User).options(selectinload(User.groups)) class User(db.Model, TimeStampMixin, SCIMEntity): @@ -697,7 +697,7 @@ def get_by_name(name: str) -> Optional["User"]: @staticmethod def get_by_id(uid: int) -> Optional["User"]: - return db.session.get(User, uid, options=[joinedload(User.groups)]) + return db.session.get(User, uid, options=[selectinload(User.groups)]) @staticmethod def get_by_email(email: str) -> Optional["User"]: @@ -816,8 +816,8 @@ def _get_personal_folders(self) -> list[Folder]: .with_only_columns(Folder) .options( defaultload(Folder._block) - .joinedload(Block.accesses) - .joinedload(BlockAccess.usergroup) + .selectinload(Block.accesses) + .selectinload(BlockAccess.usergroup) ) ) @@ -1425,7 +1425,7 @@ def set_notify_settings( def get_answers_for_task(self, task_id: str): return ( - self.answers.options(joinedload(Answer.users_all)) + self.answers.options(selectinload(Answer.users_all)) .order_by(Answer.id.desc()) .filter_by(task_id=task_id) ) diff --git a/timApp/user/usergroup.py b/timApp/user/usergroup.py index 290363e600..b2c77f192b 100644 --- a/timApp/user/usergroup.py +++ b/timApp/user/usergroup.py @@ -5,7 +5,7 @@ import attr from sqlalchemy import select -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.sql import Select @@ -345,8 +345,8 @@ def get_usergroup_eager_query() -> Select: return ( select(UserGroup) - .options(joinedload(UserGroup.admin_doc).joinedload(Block.docentries)) - .options(joinedload(UserGroup.current_memberships)) + .options(selectinload(UserGroup.admin_doc).selectinload(Block.docentries)) + .options(selectinload(UserGroup.current_memberships)) ) diff --git a/timApp/user/users.py b/timApp/user/users.py index b9744936dc..eda6de99ea 100644 --- a/timApp/user/users.py +++ b/timApp/user/users.py @@ -1,7 +1,7 @@ from collections import defaultdict from sqlalchemy import func, select -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from timApp.auth.accesstype import AccessType from timApp.auth.auth_models import BlockAccess @@ -43,11 +43,11 @@ def get_rights_holders_all(block_ids: list[int], order_by=None): result: list[tuple[BlockAccess, UserGroup, User | None]] = db.session.execute( select(BlockAccess) .options( - joinedload(BlockAccess.usergroup) - .joinedload(UserGroup.admin_doc) - .joinedload(Block.docentries) + selectinload(BlockAccess.usergroup) + .selectinload(UserGroup.admin_doc) + .selectinload(Block.docentries) ) - .options(joinedload(BlockAccess.atype)) + .options(selectinload(BlockAccess.atype)) .filter(BlockAccess.block_id.in_(block_ids)) .join(UserGroup) .outerjoin(User, User.name == UserGroup.name) diff --git a/timApp/util/flask/search.py b/timApp/util/flask/search.py index 018a1095f3..eb2f41e09c 100644 --- a/timApp/util/flask/search.py +++ b/timApp/util/flask/search.py @@ -13,7 +13,7 @@ from flask import Blueprint, json, Request from flask import request from sqlalchemy import select -from sqlalchemy.orm import joinedload, defaultload +from sqlalchemy.orm import selectinload, defaultload from timApp.auth.accesshelper import has_view_access, verify_admin, has_edit_access from timApp.auth.sessioninfo import get_current_user_object @@ -363,7 +363,7 @@ def validate_query(query: str, search_whole_words: bool) -> None: # Query options for loading DocEntry relevance eagerly; it should speed up search cache processing because # we know we'll need relevance. docentry_eager_relevance_opt = ( - defaultload(DocEntry._block).joinedload(Block.relevance), + defaultload(DocEntry._block).selectinload(Block.relevance), ) @@ -737,7 +737,7 @@ def fetch_search_items(search_items: dict, search_folder: str) -> list[DocInfo]: (DocEntry.id.in_(search_items.keys())) & (DocEntry.name.like(search_folder + "%")) ) - .options(joinedload(DocEntry._block).joinedload(Block.relevance)) + .options(selectinload(DocEntry._block).selectinload(Block.relevance)) ) .scalars() .all() diff --git a/timApp/util/get_fields.py b/timApp/util/get_fields.py index 35c32d4b97..a3108f9413 100644 --- a/timApp/util/get_fields.py +++ b/timApp/util/get_fields.py @@ -13,7 +13,7 @@ from isodate import datetime_isoformat from marshmallow import missing from sqlalchemy import func, true, select -from sqlalchemy.orm import lazyload, joinedload +from sqlalchemy.orm import lazyload, selectinload from timApp.answer.answer import Answer from timApp.answer.answers import ( @@ -387,7 +387,7 @@ def get_fields_and_users( q = q1 q = q.with_only_columns(User).order_by(User.id).options(lazyload(User.groups)) if member_filter_type != MembershipFilter.Current: - q = q.options(joinedload(User.memberships)) + q = q.options(selectinload(User.memberships)) users: list[User] = db.session.execute(q).scalars().all() user_map = {} for u in users: diff --git a/timApp/velp/annotations.py b/timApp/velp/annotations.py index d28f59ea95..5f5373a23e 100644 --- a/timApp/velp/annotations.py +++ b/timApp/velp/annotations.py @@ -10,7 +10,7 @@ from enum import Enum, unique from sqlalchemy import func, true, select -from sqlalchemy.orm import joinedload, contains_eager +from sqlalchemy.orm import selectinload, contains_eager from sqlalchemy.sql import Select from timApp.answer.answer import Answer @@ -109,28 +109,22 @@ def get_annotations_with_comments_in_document( def set_annotation_query_opts(q: Select) -> Select: return ( - q.options( - joinedload(Annotation.velp_content, innerjoin=True).load_only( - VelpContent.content - ) - ) + q.options(selectinload(Annotation.velp_content).load_only(VelpContent.content)) .options( - joinedload(Annotation.comments) - .joinedload(AnnotationComment.commenter, innerjoin=True) + selectinload(Annotation.comments) + .selectinload(AnnotationComment.commenter) .raiseload(User.groups) ) + .options(selectinload(Annotation.annotator).raiseload(User.groups)) .options( - joinedload(Annotation.annotator, innerjoin=True).raiseload(User.groups) - ) - .options( - joinedload(Annotation.answer) - .joinedload(Answer.users_all) + selectinload(Annotation.answer) + .selectinload(Answer.users_all) .raiseload(User.groups) ) .options( - joinedload(Annotation.velp_version, innerjoin=True) + selectinload(Annotation.velp_version) .load_only(VelpVersion.id, VelpVersion.velp_id) - .joinedload(VelpVersion.velp) + .selectinload(VelpVersion.velp) .load_only(Velp.color) ) ) diff --git a/timApp/velp/velp_models.py b/timApp/velp/velp_models.py index 669833d040..49ed6c7b7b 100644 --- a/timApp/velp/velp_models.py +++ b/timApp/velp/velp_models.py @@ -148,7 +148,7 @@ class VelpGroup(db.Model): ) block: Block = db.relationship( "Block", - lazy="joined", + lazy="selectin", ) # docentry = db.relationship( # 'DocEntry', diff --git a/timApp/velp/velps.py b/timApp/velp/velps.py index c5ee0ea4ff..dd6f856876 100644 --- a/timApp/velp/velps.py +++ b/timApp/velp/velps.py @@ -10,7 +10,7 @@ from typing import Iterable from sqlalchemy import func, delete, select -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import selectinload from timApp.timdb.sqa import db from timApp.velp.velp_models import ( @@ -251,12 +251,8 @@ def get_velp_content_for_document( & (VelpGroupsInDocument.doc_id == doc_id) ) .with_only_columns(Velp) - .options(joinedload(Velp.groups, innerjoin=True).raiseload(VelpGroup.block)) - .options( - joinedload(Velp.velp_versions, innerjoin=True).joinedload( - VelpVersion.content, innerjoin=True - ) - ) + .options(selectinload(Velp.groups).raiseload(VelpGroup.block)) + .options(selectinload(Velp.velp_versions).selectinload(VelpVersion.content)) ) return vq.all() From ee44794e4531970b72af988ef2a8c4bac7c0f666 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Wed, 19 Jul 2023 17:01:06 +0300 Subject: [PATCH 05/34] Fix tests for Flask 2.4; prepare for SQLAlchemy 2.0 --- cli/templates/docker/docker-compose.tmpl.yml | 1 + timApp/admin/answer_cli.py | 92 ++-- timApp/admin/change_group_email.py | 66 ++- timApp/answer/answer.py | 4 + timApp/answer/answer_models.py | 6 + timApp/answer/answers.py | 42 +- timApp/answer/feedbackanswer.py | 6 +- timApp/answer/routes.py | 159 ++++--- timApp/auth/auth_models.py | 4 + timApp/auth/login.py | 4 +- timApp/auth/oauth2/models.py | 5 +- timApp/auth/session/model.py | 1 + timApp/auth/session/util.py | 5 +- timApp/auth/sessioninfo.py | 4 +- timApp/celery_sqlalchemy_scheduler/models.py | 7 +- timApp/document/changelog.py | 1 - timApp/document/docentry.py | 5 +- timApp/document/translation/language.py | 1 + timApp/document/translation/routes.py | 1 + timApp/document/translation/translation.py | 4 +- timApp/document/translation/translator.py | 2 + timApp/folder/folder.py | 3 +- timApp/item/block.py | 13 +- timApp/item/blockassociation.py | 1 + timApp/item/blockrelevance.py | 2 + timApp/item/distribute_rights.py | 17 +- timApp/item/item.py | 6 +- timApp/item/manage.py | 2 +- timApp/item/routes.py | 1 - timApp/item/tag.py | 3 +- timApp/item/taskblock.py | 2 + timApp/lecture/askedjson.py | 2 + timApp/lecture/askedquestion.py | 8 +- timApp/lecture/lecture.py | 6 +- timApp/lecture/lectureanswer.py | 6 +- timApp/lecture/lectureusers.py | 2 + timApp/lecture/message.py | 2 + timApp/lecture/question.py | 2 + timApp/lecture/questionactivity.py | 2 + timApp/lecture/runningquestion.py | 2 + timApp/lecture/showpoints.py | 2 + timApp/lecture/useractivity.py | 2 + .../messagelist/messagelist_models.py | 3 + .../timMessage/internalmessage_models.py | 3 + timApp/messaging/timMessage/routes.py | 2 +- timApp/note/notes.py | 2 +- timApp/note/usernote.py | 2 + timApp/notification/notification.py | 2 + timApp/notification/pending_notification.py | 1 + timApp/peerreview/peerreview.py | 2 + timApp/peerreview/util/groups.py | 2 +- timApp/plugin/calendar/calendar.py | 16 +- timApp/plugin/calendar/models.py | 14 + timApp/plugin/importdata/importData.py | 2 +- timApp/plugin/jsrunner/util.py | 2 +- timApp/plugin/pluginControl.py | 2 +- timApp/plugin/plugintype.py | 4 +- timApp/plugin/timtable/row_owner_info.py | 2 + timApp/printing/documentprinter.py | 6 +- .../printing/pandoc_imagefilepathsfilter.py | 4 +- timApp/printing/printeddoc.py | 2 + timApp/readmark/readparagraph.py | 4 + timApp/readmark/routes.py | 4 +- timApp/scheduling/scheduling_routes.py | 2 +- timApp/sisu/scim.py | 2 +- timApp/sisu/scimusergroup.py | 2 + timApp/slide/slidestatus.py | 2 + timApp/tests/browser/browsertest.py | 31 +- .../csplugin/python_after_answer.png | Bin 9063 -> 9168 bytes .../csplugin/python_after_answer_switch.png | Bin 7333 -> 7456 bytes .../csplugin/python_before_answer.png | Bin 7333 -> 7456 bytes .../imagex/canvas_draw.png | Bin 3734 -> 3734 bytes .../imagex/canvas_legacy_init.png | Bin 3375 -> 3375 bytes .../imagex/canvas_modern_init.png | Bin 3734 -> 3734 bytes .../jsrunner/area_visibility_no_click.png | Bin 18962 -> 18962 bytes .../area_visibility_no_click_refresh.png | Bin 12973 -> 12973 bytes .../jsrunner/area_visibility_yes_click.png | Bin 22737 -> 22737 bytes .../area_visibility_yes_click_refresh.png | Bin 17089 -> 17089 bytes .../pareditor/ace_hello_world_3.png | Bin 35214 -> 35214 bytes .../pareditor/autocomplete.png | Bin 34635 -> 34635 bytes .../pareditor/autocomplete_no_document.png | Bin 31217 -> 31217 bytes .../pareditor/textarea_hello_world.png | Bin 35068 -> 35196 bytes .../qst_matrix-checkbox_answered.png | Bin 28009 -> 28009 bytes .../sisu/blocked_dialog_alt.png | Bin 7330 -> 7330 bytes .../timtable/timTableAllStyles.png | Bin 31915 -> 31915 bytes .../timtable/timTableExtraStyles.png | Bin 32035 -> 32035 bytes .../timtable/timTableSvgMath.png | Bin 730 -> 730 bytes .../timtable/timTableTableStyles.png | Bin 21202 -> 21202 bytes .../velps/create_new_velp_empty.png | Bin 14339 -> 14339 bytes .../velps/create_new_velp_filled.png | Bin 12910 -> 12910 bytes .../velps/filtered_canvas.png | Bin 2990 -> 2990 bytes .../velps/unfiltered_canvas.png | Bin 5330 -> 5330 bytes .../velps/velp_filter_tab.png | Bin 2783 -> 2783 bytes .../velps/velp_groups_tab.png | Bin 9165 -> 9165 bytes .../velps/velp_menu_advanced_controls.png | Bin 10196 -> 10196 bytes timApp/tests/browser/test_velps.py | 10 +- timApp/tests/db/test_references.py | 5 - .../{unit => db}/test_translator_generic.py | 0 timApp/tests/db/timdbtest.py | 37 +- timApp/tests/server/test_access_lock.py | 28 +- timApp/tests/server/test_authors.py | 9 +- timApp/tests/server/test_bookmarks.py | 6 +- timApp/tests/server/test_broken_db.py | 8 +- timApp/tests/server/test_caching.py | 10 +- timApp/tests/server/test_calendar.py | 2 +- timApp/tests/server/test_cbcountfield.py | 37 +- timApp/tests/server/test_folders.py | 82 ++-- timApp/tests/server/test_groups.py | 1 + timApp/tests/server/test_importdata.py | 33 +- timApp/tests/server/test_jsrunner.py | 7 +- timApp/tests/server/test_math.py | 5 +- timApp/tests/server/test_minutes.py | 9 +- timApp/tests/server/test_peer_review.py | 8 +- timApp/tests/server/test_permissions.py | 10 +- .../{db => server}/test_personal_folder.py | 0 timApp/tests/server/test_plugins.py | 18 +- timApp/tests/server/test_plugins_preamble.py | 7 + timApp/tests/server/test_preamble.py | 1 + timApp/tests/server/test_printing.py | 2 +- timApp/tests/server/test_question.py | 333 +++++++------- .../tests/server/test_scheduled_functions.py | 14 +- timApp/tests/server/test_showfile.py | 46 +- timApp/tests/server/test_signup.py | 8 +- timApp/tests/server/test_translation.py | 37 +- timApp/tests/server/test_upload.py | 2 +- timApp/tests/server/test_velp.py | 8 +- timApp/tests/server/timroutetest.py | 414 ++++++++++-------- timApp/tests/timliveserver.py | 16 - timApp/tim_app.py | 8 +- timApp/timdb/dbaccess.py | 16 - timApp/timdb/init.py | 10 +- timApp/timdb/sqa.py | 26 +- timApp/upload/upload.py | 9 +- timApp/user/consentchange.py | 2 + timApp/user/groups.py | 2 +- timApp/user/hakaorganization.py | 2 + timApp/user/newuser.py | 2 + timApp/user/personaluniquecode.py | 2 + timApp/user/user.py | 56 ++- timApp/user/usercontact.py | 1 + timApp/user/usergroup.py | 8 +- timApp/user/usergroupdoc.py | 2 + timApp/user/usergroupmember.py | 1 + timApp/user/userutils.py | 1 + timApp/user/verification/verification.py | 1 + timApp/util/file_utils.py | 26 ++ timApp/util/get_fields.py | 37 +- timApp/velp/annotation_model.py | 2 + timApp/velp/annotations.py | 27 +- timApp/velp/velp_models.py | 26 ++ timApp/velp/velps.py | 4 +- tim_common/timjsonencoder.py | 2 +- 152 files changed, 1224 insertions(+), 888 deletions(-) rename timApp/tests/{unit => db}/test_translator_generic.py (100%) rename timApp/tests/{db => server}/test_personal_folder.py (100%) delete mode 100644 timApp/tests/timliveserver.py create mode 100644 timApp/util/file_utils.py diff --git a/cli/templates/docker/docker-compose.tmpl.yml b/cli/templates/docker/docker-compose.tmpl.yml index 68331d68f3..def0b66f8a 100644 --- a/cli/templates/docker/docker-compose.tmpl.yml +++ b/cli/templates/docker/docker-compose.tmpl.yml @@ -396,6 +396,7 @@ ${ partial("docker", "csplugin_mongodb.tmpl.yml") if csplugin.is_mongodb_enabled volumes: - .:/service:rw environment: + TIM_TESTING: 1 TIM_SETTINGS: testconfig.py PYTHONPATH: /service CI: ${CI:-false} diff --git a/timApp/admin/answer_cli.py b/timApp/admin/answer_cli.py index eaeb72c00a..294c034605 100644 --- a/timApp/admin/answer_cli.py +++ b/timApp/admin/answer_cli.py @@ -122,33 +122,49 @@ def delete_answers_with_ids( ) -> AnswerDeleteResult: if not isinstance(ids, list): raise TypeError("ids should be a list of answer ids") - d_ua = db.session.scalars( - delete(UserAnswer) - .where(UserAnswer.answer_id.in_(ids)) - .returning(UserAnswer.id) - .execution_options(synchronize_session=False) - ).all() - d_as = db.session.scalars( - delete(AnswerSaver) - .where(AnswerSaver.answer_id.in_(ids)) - .returning(AnswerSaver.id) - .execution_options(synchronize_session=False) - ).all() + d_ua = len( + db.session.execute( + delete(UserAnswer) + .where(UserAnswer.answer_id.in_(ids)) + .returning(UserAnswer.id) + .execution_options(synchronize_session=False) + ).all() + ) + d_as = len( + ( + db.session.execute( + delete(AnswerSaver) + .where(AnswerSaver.answer_id.in_(ids)) + .returning(AnswerSaver.user_id, AnswerSaver.answer_id) + .execution_options(synchronize_session=False) + ).all() + ) + ) anns_stmt = select(Annotation.id).filter(Annotation.answer_id.in_(ids)) - d_acs = db.session.scalars( - delete(AnnotationComment) - .where( - AnnotationComment.annotation_id.in_(anns_stmt.with_entities(Annotation.id)) + d_acs = len( + ( + db.session.execute( + delete(AnnotationComment) + .where( + AnnotationComment.annotation_id.in_( + anns_stmt.with_only_columns(Annotation.id) + ) + ) + .returning(AnnotationComment.id) + .execution_options(synchronize_session=False) + ).all() ) - .returning(AnnotationComment.id) - .execution_options(synchronize_session=False) - ).all() - d_anns = db.session.scalars( - delete(Annotation) - .where(Annotation.id.in_(anns_stmt)) - .returning(Annotation.id) - .execution_options(synchronize_session=False) - ).all() + ) + d_anns = len( + ( + db.session.execute( + delete(Annotation) + .where(Annotation.id.in_(anns_stmt)) + .returning(Annotation.id) + .execution_options(synchronize_session=False) + ).all() + ) + ) ans_items_stmt = select(Answer).filter(Answer.id.in_(ids)) if verbose: click.echo( @@ -159,12 +175,14 @@ def delete_answers_with_ids( ] ) ) - d_ans = db.session.scalars( - delete(Answer) - .where(Answer.id.in_(ans_items_stmt.with_only_columns([Answer.id]))) - .returning(Answer.id) - .execution_options(synchronize_session=False) - ).all() + d_ans = len( + db.session.execute( + delete(Answer) + .where(Answer.id.in_(ans_items_stmt.with_only_columns(Answer.id))) + .returning(Answer.id) + .execution_options(synchronize_session=False) + ).all() + ) return AnswerDeleteResult( useranswer=d_ua, answersaver=d_as, @@ -224,7 +242,7 @@ def truncate_large(doc: DocInfo, limit: int, to: int, dry_run: bool) -> None: click.echo("limit must be >= to") sys.exit(1) stmt = select(Answer).filter(Answer.task_id.startswith(f"{doc.id}.")) - total = db.session.scalar(stmt.with_only_columns([func.count()])) + total = db.session.scalar(stmt.with_only_columns(func.count())) anss: list[Answer] = ( db.session.execute( stmt.filter(func.length(Answer.content) > limit).options( @@ -347,13 +365,13 @@ def delete_old_answers(d: DocInfo, tasks: list[str]) -> DeleteResult: base_query = valid_answers_query( [TaskId(doc_id=d.id, task_name=t) for t in tasks] ).join(User, Answer.users) - latest = base_query.group_by(Answer.task_id, User.id).with_entities( + latest = base_query.group_by(Answer.task_id, User.id).with_only_columns( func.max(Answer.id) ) - todelete = base_query.filter(Answer.id.notin_(latest)).with_entities(Answer.id) - tot = base_query.count() - del_tot = todelete.count() - adr = delete_answers_with_ids(todelete.all()) + todelete = base_query.filter(Answer.id.notin_(latest)).with_only_columns(Answer.id) + tot = db.session.scalar(base_query.with_only_columns(func.count())) + del_tot = db.session.scalar(todelete.with_only_columns(func.count())) + adr = delete_answers_with_ids(db.session.execute(todelete).scalars().all()) r = DeleteResult(total=tot, deleted=del_tot, adr=adr) return r diff --git a/timApp/admin/change_group_email.py b/timApp/admin/change_group_email.py index 1d75574701..8e4dfbdb6d 100644 --- a/timApp/admin/change_group_email.py +++ b/timApp/admin/change_group_email.py @@ -1,50 +1,48 @@ from sqlalchemy import select -from timApp.timdb.dbaccess import get_files_path +from timApp.tim_app import app from timApp.timdb.sqa import db -from timApp.timdb.timdb import TimDb from timApp.user.user import User, UserInfo from timApp.user.usergroup import UserGroup def change_email() -> None: - timdb = TimDb(get_files_path()) - # print("You're changing email of every member of given group to [USERNAME]@[GIVEN_EMAIL]") - print("Changing emails of mallikurssinryhma1") - while True: - # groupname = input("Input group to edit: ") - groupname = "mallikurssinryhma1" - group = db.session.scalars( - select(UserGroup).filter_by(name=groupname) - .limit(1) - ).first() - users: list[User] = group.users - new_email = input("Input new email suffix: ") - print("New values:") - for user in users: - if "@malli" not in user.name: - continue - uprefix = str(user.name).replace("@malli", "") - print(user.name + ": " + uprefix + "@" + new_email) - yesno = input("Is this correct? y/n/quit: ") - if yesno == "y": + with app.app_context(): + # print("You're changing email of every member of given group to [USERNAME]@[GIVEN_EMAIL]") + print("Changing emails of mallikurssinryhma1") + while True: + # groupname = input("Input group to edit: ") + groupname = "mallikurssinryhma1" + group = ( + db.session.execute(select(UserGroup).filter_by(name=groupname).limit(1)) + .scalars() + .first() + ) + users: list[User] = group.users + new_email = input("Input new email suffix: ") + print("New values:") for user in users: if "@malli" not in user.name: continue uprefix = str(user.name).replace("@malli", "") - user.update_info( - UserInfo( - username=user.name, - full_name=user.real_name, - email=uprefix + "@" + new_email, + print(user.name + ": " + uprefix + "@" + new_email) + yesno = input("Is this correct? y/n/quit: ") + if yesno == "y": + for user in users: + if "@malli" not in user.name: + continue + uprefix = str(user.name).replace("@malli", "") + user.update_info( + UserInfo( + username=user.name, + full_name=user.real_name, + email=uprefix + "@" + new_email, + ) ) - ) - break - elif yesno == "q" or yesno == "quit": - timdb.close() - exit() - db.session.commit() - timdb.close() + break + elif yesno == "q" or yesno == "quit": + exit() + db.session.commit() if __name__ == "__main__": diff --git a/timApp/answer/answer.py b/timApp/answer/answer.py index 1729b1e75a..e72423deb0 100644 --- a/timApp/answer/answer.py +++ b/timApp/answer/answer.py @@ -15,6 +15,8 @@ class AnswerSaver(db.Model): """ __tablename__ = "answersaver" + __allow_unmapped__ = True + answer_id = db.Column(db.Integer, db.ForeignKey("answer.id"), primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) @@ -23,6 +25,8 @@ class Answer(db.Model): """An answer to a task.""" __tablename__ = "answer" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) """Answer identifier.""" diff --git a/timApp/answer/answer_models.py b/timApp/answer/answer_models.py index def285c670..819212b8cb 100644 --- a/timApp/answer/answer_models.py +++ b/timApp/answer/answer_models.py @@ -8,6 +8,8 @@ class AnswerTag(db.Model): """ __tablename__ = "answertag" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) answer_id = db.Column(db.Integer, db.ForeignKey("answer.id"), nullable=False) tag = db.Column(db.Text, nullable=False) @@ -17,6 +19,8 @@ class AnswerUpload(db.Model): """Associates uploaded files (Block with type BlockType.AnswerUpload) with Answers.""" __tablename__ = "answerupload" + __allow_unmapped__ = True + upload_block_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) answer_id = db.Column(db.Integer, db.ForeignKey("answer.id")) @@ -32,6 +36,8 @@ class UserAnswer(db.Model): """Associates Users with Answers.""" __tablename__ = "useranswer" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) answer_id = db.Column(db.Integer, db.ForeignKey("answer.id"), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) diff --git a/timApp/answer/answers.py b/timApp/answer/answers.py index 164b6a2e6f..dd80bbadda 100644 --- a/timApp/answer/answers.py +++ b/timApp/answer/answers.py @@ -54,19 +54,17 @@ def get_answers_query(task_id: TaskId, users: list[User], only_valid: bool) -> S if only_valid: stmt = stmt.filter_by(valid=True) if not task_id.is_global: - stmt = select(Answer).filter( - Answer.id.in_( - stmt.join(User, Answer.users) - .filter(User.id.in_([u.id for u in users])) - .group_by(Answer.id) - .with_only_columns([Answer.id]) - .having( - (func.array_agg(aggregate_order_by(User.id, User.id))) - == sorted(u.id for u in users) - ) - .subquery() + stmt = ( + stmt.join(User, Answer.users) + .filter(User.id.in_([u.id for u in users])) + .group_by(Answer.id) + .having( + (func.array_agg(aggregate_order_by(User.id, User.id))) + == sorted(u.id for u in users) ) + .with_only_columns(Answer.id) ) + stmt = select(Answer).filter(Answer.id.in_(stmt)) stmt = stmt.order_by(Answer.id.desc()) return stmt @@ -81,14 +79,10 @@ def get_latest_answers_query( stmt.join(User, Answer.users) .filter(User.id.in_([u.id for u in users])) .group_by(User.id) - .with_entities(func.max(Answer.id).label("aid"), User.id.label("uid")) + .with_only_columns(func.max(Answer.id).label("aid"), User.id.label("uid")) .subquery() ) - return ( - select(Answer) - .join(stmt_sub, Answer.id == stmt_sub.c.aid) - .with_only_columns(Answer) - ) + return select(Answer).join(stmt_sub, Answer.id == stmt_sub.c.aid) def is_redundant_answer( @@ -506,8 +500,8 @@ def get_existing_answers_info( users: list[User], task_id: TaskId, only_valid: bool ) -> ExistingAnswersInfo: stmt = get_answers_query(task_id, users, only_valid) - latest = db.session.scalars(stmt.limit(1)).first() - count = db.session.scalar(stmt.with_only_columns([func.count()])) + latest = db.session.execute(stmt.limit(1)).scalars().first() + count = db.session.scalar(select(func.count()).select_from(stmt.subquery())) return ExistingAnswersInfo(latest_answer=latest, count=count) @@ -663,7 +657,7 @@ def get_users_for_tasks( task_sum = ( func.round( func.sum( - case([(sub_joined.c.valid == True, sub_joined.c.points)], else_=0) + case((sub_joined.c.valid == True, sub_joined.c.points), else_=0) ).cast(Numeric), 4, ) @@ -673,7 +667,7 @@ def get_users_for_tasks( velp_sum = ( func.round( func.sum( - case([(sub_joined.c.valid == True, sub_joined.c.velp_points)], else_=0) + case((sub_joined.c.valid == True, sub_joined.c.velp_points), else_=0) ).cast(Numeric), 4, ) @@ -1116,7 +1110,5 @@ def get_global_answers(parsed_task_ids: dict[str, TaskId]) -> list[Answer]: .with_only_columns(func.max(Answer.id).label("aid")) .subquery() ) - global_datas = ( - select(Answer).join(sq2, Answer.id == sq2.c.aid).with_only_columns(Answer).all() - ) - return db.session.scalars(global_datas).all() + global_datas = select(Answer).join(sq2, Answer.id == sq2.c.aid) + return db.session.execute(global_datas).scalars().all() diff --git a/timApp/answer/feedbackanswer.py b/timApp/answer/feedbackanswer.py index 9437a74b1e..c7edf80fa0 100644 --- a/timApp/answer/feedbackanswer.py +++ b/timApp/answer/feedbackanswer.py @@ -14,6 +14,7 @@ from timApp.document.viewcontext import default_view_ctx from timApp.plugin.plugin import Plugin, find_task_ids from timApp.plugin.taskid import TaskId +from timApp.timdb.sqa import db from timApp.user.user import User from timApp.util.answerutil import get_answer_period, AnswerPeriodOptions from timApp.util.flask.requesthelper import get_option @@ -61,10 +62,10 @@ def get_all_feedback_answers( q = q.order_by(User.name, Answer.answered_on) # "q" with Answer and User data. - q = q.with_entities(Answer, User) + q = q.with_only_columns(Answer, User) # Makes q query an iterable qq for for-loop. - qq: Iterable[tuple[Answer, User]] = q + qq: Iterable[tuple[Answer, User]] = db.session.execute(q) return compile_csv(qq, printname, hide_names, exp_answers, users, dec) @@ -101,7 +102,6 @@ def compile_csv( user_ctx = user_context_with_logged_in(None) for answer, user in qq: - if prev_user != user: # Resets if previous is different user. prev_user = None prev_ans = None diff --git a/timApp/answer/routes.py b/timApp/answer/routes.py index 1c53268f58..7163d18ce9 100644 --- a/timApp/answer/routes.py +++ b/timApp/answer/routes.py @@ -187,16 +187,20 @@ def save_review_points( verify_view_access(doc) if not is_peerreview_enabled(doc): raise AccessDenied("Peer review is not enabled") - peer_review = db.session.scalars( - select(PeerReview) - .filter_by( - block_id=tid.doc_id, - task_name=tid.task_name, - reviewer_id=curr_user_id, - reviewable_id=user_id, + peer_review = ( + db.session.execute( + select(PeerReview) + .filter_by( + block_id=tid.doc_id, + task_name=tid.task_name, + reviewer_id=curr_user_id, + reviewable_id=user_id, + ) + .limit(1) ) - .limit(1) - ).first() + .scalars() + .first() + ) if not peer_review: raise RouteException("Invalid review target") try: @@ -443,10 +447,11 @@ def get_useranswers_for_task( .group_by(Answer.task_id) .subquery() ) - answs: list[Answer] = db.scalars( - select(Answer).join(sub, Answer.id == sub.c.col) - .options(selectinload(Answer.users_all)) - ).all() + answs: list[Answer] = ( + db.session.execute(select(Answer).join(sub, Answer.id == sub.c.col).options(selectinload(Answer.users_all))) + .scalars() + .all() + ) for answer in answs: asd = answer.to_json() asd.pop("points", None) @@ -468,9 +473,8 @@ def get_globals_for_tasks(task_ids: list[TaskId], answer_map: dict[str, dict]) - select(Answer) .join(sub, Answer.id == sub.c.col) .with_only_columns(Answer, sub.c.cnt) - .all() ) - for answer, _ in db.session.scalars(answers_all): + for answer, _ in db.session.execute(answers_all): asd = answer.to_json() answer_map[answer.task_id] = asd @@ -1284,11 +1288,16 @@ def check_answerupload_file_accesses( """ uploads: list[AnswerUpload] = [] doc_map = {} - blocks = db.session.scalars( - select(Block).filter( - Block.description.in_(filelist) & (Block.type_id == BlockType.Upload.value) + blocks = ( + db.session.execute( + select(Block).filter( + Block.description.in_(filelist) + & (Block.type_id == BlockType.Upload.value) + ) ) - ).all() + .scalars() + .all() + ) if len(blocks) != len(filelist): block_filelist = [b.description for b in blocks] for f in filelist: @@ -1575,7 +1584,7 @@ def export_answers(doc_path: str) -> Response: if not d: raise RouteException("Document not found") verify_teacher_access(d) - answer_list: list[tuple[Answer, str]] = db.session.scalars( + answer_list: list[tuple[Answer, str]] = db.session.execute( select(Answer) .filter(Answer.task_id.startswith(f"{d.id}.")) .join(User, Answer.users) @@ -1614,9 +1623,11 @@ def import_answers( raise NotFound(f"No group with name '{group}'") verify_group_view_access(ug) doc_paths = {doc_map.get(a.doc, a.doc) for a in exported_answers} - docs = db.session.scalars( - select(DocEntry).filter(DocEntry.name.in_(doc_paths)) - ).all() + docs = ( + db.session.execute(select(DocEntry).filter(DocEntry.name.in_(doc_paths))) + .scalars() + .all() + ) doc_path_map = {d.path: d for d in docs} missing_docs = doc_paths - set(doc_path_map) if missing_docs: @@ -1641,7 +1652,7 @@ def import_answers( f"Found: {seq_to_str([str((a.email, a.username)) for a in mixed_answers])}" ) - existing_answers: list[tuple[Answer, str]] = db.session.scalars( + existing_answers: list[tuple[Answer, str]] = db.session.execute( select(Answer) .filter(filter_cond) .join(User, Answer.users) @@ -1669,12 +1680,16 @@ def convert_email_case(email: str | None) -> str | None: dupes = 0 # noinspection PyUnresolvedReferences - all_users = db.session.scalars( - select(User).filter( - email_field.in_([a.email for a in exported_answers if a.email]) - | name_field.in_([a.username for a in exported_answers if a.username]) + all_users = ( + db.session.execute( + select(User).filter( + email_field.in_([a.email for a in exported_answers if a.email]) + | name_field.in_([a.username for a in exported_answers if a.username]) + ) ) - ).all() + .scalars() + .all() + ) if not match_email_case: all_emails = defaultdict(list) @@ -1789,12 +1804,16 @@ def get_answers(task_id: str, user_id: int) -> Response: if tid.is_global: verify_view_access(d) user_context = user_context_with_logged_in(curr_user) - user_answers = db.session.scalars( - select(Answer) - .filter_by(task_id=tid.doc_task) - .order_by(Answer.id.desc()) - .options(selectinload(Answer.users_all)) - ).all() + user_answers = ( + db.session.execute( + select(Answer) + .filter_by(task_id=tid.doc_task) + .order_by(Answer.id.desc()) + .options(selectinload(Answer.users_all)) + ) + .scalars() + .all() + ) user = curr_user else: user = User.get_by_id(user_id) @@ -2192,7 +2211,7 @@ def get_task_users(task_id: str, peer_review: bool = False) -> Response: stmt = stmt.join(UserGroup, User.groups).filter( UserGroup.name.in_(usergroups) ) - users = db.session.scalars(stmt).all() + users = db.session.execute(stmt).scalars().all() if hide_names_in_teacher(d): model_u = User.get_model_answer_user() for user in users: @@ -2217,9 +2236,11 @@ def rename_answers(old_name: str, new_name: str, doc_path: str) -> Response: raise RouteException( f"The new name conflicts with {conflicts} other answers with the same task name." ) - answers_to_rename = db.session.scalars( - select(Answer).filter_by(task_id=f"{d.id}.{old_name}") - ).all() + answers_to_rename = ( + db.session.execute(select(Answer).filter_by(task_id=f"{d.id}.{old_name}")) + .scalars() + .all() + ) for a in answers_to_rename: a.task_id = f"{d.id}.{new_name}" db.session.commit() @@ -2243,15 +2264,19 @@ def clear_task_block(user: str, task_id: str) -> Response: b = TaskBlock.get_by_task(tid.doc_task) if not b: return json_response({"cleared": False}) - ba = db.session.scalars( - select(BlockAccess) - .filter_by( - block_id=b.id, - type=AccessType.view.value, - usergroup_id=user_obj.get_personal_group().id, + ba = ( + db.session.execute( + select(BlockAccess) + .filter_by( + block_id=b.id, + type=AccessType.view.value, + usergroup_id=user_obj.get_personal_group().id, + ) + .limit(1) ) - .limit(1) - ).first() + .scalars() + .first() + ) if not ba or not ba.accessible_to: return json_response({"cleared": False}) ba.accessible_to = None @@ -2290,15 +2315,19 @@ def unlock_locked_task(task_id: str) -> Response: if prerequisite_info.requireLock: b = TaskBlock.get_by_task(prerequisite_taskid.doc_task) if b: - ba = db.session.scalars( - select(BlockAccess) - .filter_by( - block_id=b.id, - type=AccessType.view.value, - usergroup_id=current_user.get_personal_group().id, + ba = ( + db.session.execute( + select(BlockAccess) + .filter_by( + block_id=b.id, + type=AccessType.view.value, + usergroup_id=current_user.get_personal_group().id, + ) + .limit(1) ) - .limit(1) - ).first() + .scalars() + .first() + ) if ba and ba.accessible_to and ba.accessible_to < get_current_time(): return json_response({"unlocked": True}) return json_response( @@ -2340,15 +2369,19 @@ def unlock_task(task_id: str) -> Response: if not b: b = insert_task_block(task_id=tid.doc_task, owner_groups=d.owners) else: - ba = db.session.scalars( - select(BlockAccess) - .filter_by( - block_id=b.id, - type=AccessType.view.value, - usergroup_id=current_user.get_personal_group().id, + ba = ( + db.session.execute( + select(BlockAccess) + .filter_by( + block_id=b.id, + type=AccessType.view.value, + usergroup_id=current_user.get_personal_group().id, + ) + .limit(1) ) - .limit(1) - ).first() + .scalars() + .first() + ) if not ba: time_now = get_current_time() expire_time = time_now + timedelta(seconds=access_duration) diff --git a/timApp/auth/auth_models.py b/timApp/auth/auth_models.py index aacb5261fd..5ae7358b13 100644 --- a/timApp/auth/auth_models.py +++ b/timApp/auth/auth_models.py @@ -16,6 +16,8 @@ class AccessTypeModel(db.Model): """A kind of access that a UserGroup may have to a Block.""" __tablename__ = "accesstype" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) """Access type identifier.""" @@ -35,6 +37,8 @@ class BlockAccess(db.Model): """A single permission. Relates a UserGroup with a Block along with an AccessType.""" __tablename__ = "blockaccess" + __allow_unmapped__ = True + block_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) usergroup_id = db.Column( db.Integer, db.ForeignKey("usergroup.id"), primary_key=True diff --git a/timApp/auth/login.py b/timApp/auth/login.py index 91fb75748f..fdc08187b3 100644 --- a/timApp/auth/login.py +++ b/timApp/auth/login.py @@ -9,7 +9,7 @@ from flask import request from flask import session from flask import url_for -from flask.sessions import SecureCookieSession +from flask.sessions import SessionMixin from sqlalchemy import select, delete from timApp.admin.user_cli import do_merge_users, do_soft_delete @@ -598,7 +598,7 @@ def quick_login(username: str) -> Response: return update_locale_lang(safe_redirect(url_for("view_page.index_page"))) -def log_in_as_anonymous(sess: SecureCookieSession) -> User: +def log_in_as_anonymous(sess: SessionMixin) -> User: user_name = "Anonymous" user_real_name = "Guest" user = create_anonymous_user(user_name, user_real_name) diff --git a/timApp/auth/oauth2/models.py b/timApp/auth/oauth2/models.py index 84ddb6091a..3adf413874 100644 --- a/timApp/auth/oauth2/models.py +++ b/timApp/auth/oauth2/models.py @@ -1,6 +1,5 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Optional from authlib.integrations.sqla_oauth2 import ( OAuth2TokenMixin, @@ -103,6 +102,8 @@ def check_grant_type(self, grant_type: str) -> bool: class OAuth2Token(db.Model, OAuth2TokenMixin): __tablename__ = "oauth2_token" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id")) user = db.relationship("User") @@ -110,6 +111,8 @@ class OAuth2Token(db.Model, OAuth2TokenMixin): class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): __tablename__ = "oauth2_auth_code" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id")) user = db.relationship("User") diff --git a/timApp/auth/session/model.py b/timApp/auth/session/model.py index 932b4d33cb..7c92e56a27 100755 --- a/timApp/auth/session/model.py +++ b/timApp/auth/session/model.py @@ -20,6 +20,7 @@ class UserSession(db.Model): """ __tablename__ = "usersession" + __allow_unmapped__ = True user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) """ diff --git a/timApp/auth/session/util.py b/timApp/auth/session/util.py index fd9677a619..43779d1554 100644 --- a/timApp/auth/session/util.py +++ b/timApp/auth/session/util.py @@ -182,7 +182,7 @@ def verify_session_for(username: str, session_id: str | None = None) -> None: :param username: Username of the user to verify the session for. :param session_id: If specified, verify the specific session ID. If None, verify the latest added session. """ - user_subquery = select(User.id).filter(User.name == username).subquery() + user_subquery = select(User.id).filter(User.name == username) stmt_base = ( update(UserSession) .where(UserSession.user_id.in_(user_subquery)) @@ -199,7 +199,6 @@ def verify_session_for(username: str, session_id: str | None = None) -> None: .filter(UserSession.user_id.in_(user_subquery)) .order_by(UserSession.logged_in_at.desc()) .limit(1) - .subquery() ) stmt_expire = stmt_base.where(UserSession.session_id.notin_(subquery)) stmt_verify = stmt_base.where(UserSession.session_id.in_(subquery)) @@ -218,7 +217,7 @@ def invalidate_sessions_for(username: str, session_id: str | None = None) -> Non :param username: Username of the user to invalidate the session for. :param session_id: If specified, invalidate the specific session ID. If None, invalidate all sessions. """ - user_subquery = select(User.id).filter(User.name == username).subquery() + user_subquery = select(User.id).filter(User.name == username) stmt_invalidate = ( update(UserSession) .filter(UserSession.user_id.in_(user_subquery)) diff --git a/timApp/auth/sessioninfo.py b/timApp/auth/sessioninfo.py index 2d9c6b7728..4ad4d840c6 100644 --- a/timApp/auth/sessioninfo.py +++ b/timApp/auth/sessioninfo.py @@ -2,7 +2,7 @@ from flask import session, g, request, has_request_context from sqlalchemy import select -from sqlalchemy.orm import selectinload +from sqlalchemy.orm import joinedload from timApp.document.usercontext import UserContext from timApp.timdb.sqa import db @@ -27,7 +27,7 @@ def get_current_user_object() -> User: u: User | None = db.session.get( User, curr_id, - options=[selectinload(User.lectures), selectinload(User.groups)], + options=[joinedload(User.lectures), joinedload(User.groups)], ) if u is None: if curr_id != 0: diff --git a/timApp/celery_sqlalchemy_scheduler/models.py b/timApp/celery_sqlalchemy_scheduler/models.py index ebf4cc7a99..eda290dbfb 100644 --- a/timApp/celery_sqlalchemy_scheduler/models.py +++ b/timApp/celery_sqlalchemy_scheduler/models.py @@ -37,6 +37,7 @@ def update(self, **kw): class IntervalSchedule(ModelBase, ModelMixin): __tablename__ = "celery_interval_schedule" __table_args__ = {"sqlite_autoincrement": True} + __allow_unmapped__ = True DAYS = "days" HOURS = "hours" @@ -86,6 +87,7 @@ def period_singular(self): class CrontabSchedule(ModelBase, ModelMixin): __tablename__ = "celery_crontab_schedule" __table_args__ = {"sqlite_autoincrement": True} + __allow_unmapped__ = True id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) minute = sa.Column(sa.String(60 * 4), default="*") @@ -142,6 +144,7 @@ def from_schedule(cls, session, schedule): class SolarSchedule(ModelBase, ModelMixin): __tablename__ = "celery_solar_schedule" __table_args__ = {"sqlite_autoincrement": True} + __allow_unmapped__ = True id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) @@ -179,6 +182,7 @@ class PeriodicTaskChanged(ModelBase, ModelMixin): """Helper table for tracking updates to periodic tasks.""" __tablename__ = "celery_periodic_task_changed" + __allow_unmapped__ = True id = sa.Column(sa.Integer, primary_key=True) last_update = sa.Column( @@ -203,7 +207,7 @@ def update_changed(cls, mapper, connection, target): :param target: the mapped instance being persisted """ s = connection.execute( - select([PeriodicTaskChanged]).where(PeriodicTaskChanged.id == 1).limit(1) + select(PeriodicTaskChanged).where(PeriodicTaskChanged.id == 1).limit(1) ) if not s: s = connection.execute( @@ -230,6 +234,7 @@ def last_change(cls, session): class PeriodicTask(ModelBase, ModelMixin): __tablename__ = "celery_periodic_task" __table_args__ = {"sqlite_autoincrement": True} + __allow_unmapped__ = True id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) block_id = sa.Column(sa.Integer, sa.ForeignKey("block.id"), nullable=True) diff --git a/timApp/document/changelog.py b/timApp/document/changelog.py index 695f0ba03e..ee5dab6e01 100644 --- a/timApp/document/changelog.py +++ b/timApp/document/changelog.py @@ -66,7 +66,6 @@ def get_authorinfo(self, pars: list[DocParagraph]) -> dict[str, AuthorInfo]: .filter(UserGroup.id.in_(usergroup_ids)) .outerjoin(User, User.name == UserGroup.name) ) - .scalars() .all() ) # type: List[Tuple[UserGroup, Optional[User]]] for ug, u in result: diff --git a/timApp/document/docentry.py b/timApp/document/docentry.py index 757cf20ced..88fde5311e 100644 --- a/timApp/document/docentry.py +++ b/timApp/document/docentry.py @@ -29,6 +29,8 @@ class DocEntry(db.Model, DocInfo): """ __tablename__ = "docentry" + __allow_unmapped__ = True + name = db.Column(db.Text, primary_key=True) """Full path of the document. @@ -41,7 +43,7 @@ class DocEntry(db.Model, DocInfo): public = db.Column(db.Boolean, nullable=False, default=True) """Whether the document is visible in directory listing.""" - _block = db.relationship("Block", back_populates="docentries", lazy="selectin") + _block = db.relationship("Block", back_populates="docentries", lazy="joined") trs: list[Translation] = db.relationship( "Translation", @@ -52,6 +54,7 @@ class DocEntry(db.Model, DocInfo): # TODO: This feels slightly hacky. This relationship attribute might be better in Block class, although that # doesn't sound ideal either. passive_deletes="all", + cascade_backrefs=False, ) __table_args__ = (db.Index("docentry_id_idx", "id"),) diff --git a/timApp/document/translation/language.py b/timApp/document/translation/language.py index d6e07be0f2..876c3100e6 100644 --- a/timApp/document/translation/language.py +++ b/timApp/document/translation/language.py @@ -33,6 +33,7 @@ class Language(db.Model): """ __tablename__ = "language" + __allow_unmapped__ = True lang_code = db.Column(db.Text, nullable=False, primary_key=True) """Standardized code of the language.""" diff --git a/timApp/document/translation/routes.py b/timApp/document/translation/routes.py index 6413ea0e6c..1f9869c9ff 100644 --- a/timApp/document/translation/routes.py +++ b/timApp/document/translation/routes.py @@ -207,6 +207,7 @@ def create_translation_route( cite_doc = create_document_and_block(get_current_user_object().get_personal_group()) tr = Translation(doc_id=cite_doc.doc_id, src_docid=src_doc.doc_id, lang_id=language) + db.session.add(tr) tr.title = title add_reference_pars( diff --git a/timApp/document/translation/translation.py b/timApp/document/translation/translation.py index 566ca58f40..fa869c5879 100644 --- a/timApp/document/translation/translation.py +++ b/timApp/document/translation/translation.py @@ -15,6 +15,8 @@ class Translation(db.Model, DocInfo): """ __tablename__ = "translation" + __allow_unmapped__ = True + doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) src_docid = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) lang_id = db.Column(db.Text, nullable=False) @@ -64,8 +66,8 @@ def to_json(self, **kwargs): def add_tr_entry(doc_id: int, item: DocInfo, tr: Translation) -> Translation: new_tr = Translation(doc_id=doc_id, src_docid=item.id, lang_id=tr.lang_id) + db.session.add(new_tr) new_tr.title = tr.title # Set docentry so that it can be used without extra queries in other methods new_tr.docentry = item - db.session.add(new_tr) return new_tr diff --git a/timApp/document/translation/translator.py b/timApp/document/translation/translator.py index 6075d5d8f8..f4858ced34 100644 --- a/timApp/document/translation/translator.py +++ b/timApp/document/translation/translator.py @@ -75,6 +75,7 @@ class TranslationService(db.Model): """ __tablename__ = "translationservice" + __allow_unmapped__ = True id = db.Column(db.Integer, primary_key=True) """Translation service identifier.""" @@ -178,6 +179,7 @@ class TranslationServiceKey(db.Model): """ __tablename__ = "translationservicekey" + __allow_unmapped__ = True id = db.Column(db.Integer, primary_key=True) """Key identifier.""" diff --git a/timApp/folder/folder.py b/timApp/folder/folder.py index a7a20cc62c..fc1ddab405 100644 --- a/timApp/folder/folder.py +++ b/timApp/folder/folder.py @@ -26,6 +26,7 @@ class Folder(db.Model, Item): """Represents a folder in the directory hierarchy.""" __tablename__ = "folder" + __allow_unmapped__ = True id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) """Folder identifier.""" @@ -38,7 +39,7 @@ class Folder(db.Model, Item): __table_args__ = (db.UniqueConstraint("name", "location", name="folder_uc"),) - _block = db.relationship("Block", back_populates="folder", lazy="selectin") + _block = db.relationship("Block", back_populates="folder", lazy="joined") @staticmethod def get_root() -> Folder: diff --git a/timApp/item/block.py b/timApp/item/block.py index e1a16ff0cb..e4de9a542d 100644 --- a/timApp/item/block.py +++ b/timApp/item/block.py @@ -28,6 +28,8 @@ class Block(db.Model): """The "base class" for all database objects that are part of the permission system.""" __tablename__ = "block" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) """A unique identifier for the Block.""" @@ -49,15 +51,16 @@ class Block(db.Model): """When this Block was last modified.""" docentries = db.relationship("DocEntry", back_populates="_block") - folder = db.relationship("Folder", back_populates="_block", uselist=False) + folder = db.relationship("Folder", back_populates="_block", uselist=False, cascade_backrefs=False) translation = db.relationship( "Translation", back_populates="_block", uselist=False, foreign_keys="Translation.doc_id", + cascade_backrefs=False, ) answerupload = db.relationship( - "AnswerUpload", back_populates="block", lazy="dynamic" + "AnswerUpload", back_populates="block", lazy="dynamic", cascade_backrefs=False ) accesses = db.relationship( "BlockAccess", @@ -65,6 +68,7 @@ class Block(db.Model): lazy="selectin", cascade="all, delete-orphan", collection_class=attribute_mapped_collection("block_collection_key"), + cascade_backrefs=False, ) tags: list[Tag] = db.relationship("Tag", back_populates="block", lazy="select") children = db.relationship( @@ -106,10 +110,10 @@ class Block(db.Model): ) internalmessage: InternalMessage | None = db.relationship( - "InternalMessage", back_populates="block" + "InternalMessage", back_populates="block", cascade_backrefs=False ) internalmessage_display: InternalMessageDisplay | None = db.relationship( - "InternalMessageDisplay", back_populates="display_block" + "InternalMessageDisplay", back_populates="display_block", cascade_backrefs=False ) def __json__(self): @@ -211,6 +215,7 @@ def insert_block( type=AccessType.owner.value, accessible_from=get_current_time(), ) + db.session.add(access) b.accesses[(owner_group.id, AccessType.owner.value)] = access # Also register to accesses_alt because it may be used by other methods in the same session owner_group.accesses_alt[(b.id, AccessType.owner.value)] = access diff --git a/timApp/item/blockassociation.py b/timApp/item/blockassociation.py index cd80f7d208..9156b1376b 100644 --- a/timApp/item/blockassociation.py +++ b/timApp/item/blockassociation.py @@ -5,6 +5,7 @@ class BlockAssociation(db.Model): """Associates blocks with other blocks. Currently only used for associating uploaded files with documents.""" __tablename__ = "blockassociation" + __allow_unmapped__ = True parent = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) """The parent Block.""" diff --git a/timApp/item/blockrelevance.py b/timApp/item/blockrelevance.py index 01909a56da..7a683d0322 100644 --- a/timApp/item/blockrelevance.py +++ b/timApp/item/blockrelevance.py @@ -5,6 +5,8 @@ class BlockRelevance(db.Model): """A relevance value of a block (used in search).""" __tablename__ = "blockrelevance" + __allow_unmapped__ = True + block_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) relevance = db.Column(db.Integer, nullable=False) diff --git a/timApp/item/distribute_rights.py b/timApp/item/distribute_rights.py index fa060e07f8..54c0268e00 100644 --- a/timApp/item/distribute_rights.py +++ b/timApp/item/distribute_rights.py @@ -259,8 +259,9 @@ def get_group_emails(self, r: GroupOp) -> list[Email]: if not emails: emails = [ e - for e, in db.session.execute( - select(User.email).join(User, UserGroup.users) + for e in db.session.execute( + select(User.email) + .join(User, UserGroup.users) .filter(UserGroup.name == r.group) ).scalars() ] @@ -438,9 +439,10 @@ def receive_right( ) -> Response: check_secret(secret, "DIST_RIGHTS_RECEIVE_SECRET") uges = db.session.execute( - select(UserGroup, User.email).join(User, UserGroup.name == User.name) + select(UserGroup, User.email) + .join(User, UserGroup.name == User.name) .filter(User.email.in_(re.email for re in rights)) - ).scalars().all() + ) group_map = {} for ug, email in uges: group_map[email] = ug @@ -542,8 +544,9 @@ def get_current_rights_route( raise RouteException(f"Unknown target: {target}") groups_list = groups.split(",") emails = db.session.execute( - select(User.email).join(UserGroup, User.groups) + select(User.email) + .join(UserGroup, User.groups) .filter(UserGroup.name.in_(groups_list)) .order_by(User.email) - ).scalars().all() - return json_response([{"email": e, "right": rights.get_right(e)} for e, in emails]) + ).scalars() + return json_response([{"email": e, "right": rights.get_right(e)} for e in emails]) diff --git a/timApp/item/item.py b/timApp/item/item.py index a13c053b93..c93149d03c 100644 --- a/timApp/item/item.py +++ b/timApp/item/item.py @@ -31,7 +31,7 @@ def owners(self): def block(self) -> Block: # Relationships are not loaded when constructing an object with __init__. if not hasattr(self, "_block") or self._block is None: - self._block = db.session.get(Block, self.id) + self._block = db.session.get(Block, self.id, populate_existing=True) return self._block @property @@ -125,13 +125,13 @@ def parents_to_root(self, include_root=True, eager_load_groups=False): select(Folder) .filter(tuple_(Folder.location, Folder.name).in_(path_tuples)) .order_by(func.length(Folder.location).desc()) - .options(defaultload(Folder._block).selectinload(Block.relevance)) + .options(defaultload(Folder._block).joinedload(Block.relevance)) ) if eager_load_groups: crumbs_stmt = crumbs_stmt.options( defaultload(Folder._block) .selectinload(Block.accesses) - .selectinload(BlockAccess.usergroup) + .joinedload(BlockAccess.usergroup) ) crumbs = db.session.execute(crumbs_stmt).scalars().all() if include_root: diff --git a/timApp/item/manage.py b/timApp/item/manage.py index ca49b61efc..4294660747 100644 --- a/timApp/item/manage.py +++ b/timApp/item/manage.py @@ -214,7 +214,7 @@ def group_objects(self): db.session.execute( select(UserGroup).filter(UserGroup.name.in_(self.groups)) ) - .scalar() + .scalars() .all() ) diff --git a/timApp/item/routes.py b/timApp/item/routes.py index 25b00422a8..cf8be83f04 100644 --- a/timApp/item/routes.py +++ b/timApp/item/routes.py @@ -384,7 +384,6 @@ def gen_cache( .join(UserGroup, User.groups) .filter(UserGroup.id.in_(group_ids)) ) - .scalars() .all() ) groups_that_need_access_check = { diff --git a/timApp/item/tag.py b/timApp/item/tag.py index 3ba5cdabd0..5870f65797 100644 --- a/timApp/item/tag.py +++ b/timApp/item/tag.py @@ -1,5 +1,4 @@ from enum import Enum, unique -from typing import Optional from timApp.timdb.sqa import db @@ -22,6 +21,8 @@ class Tag(db.Model): """A tag with associated document id, tag name, type and expiration date.""" __tablename__ = "tag" + __allow_unmapped__ = True + block_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) name = db.Column(db.Text, primary_key=True) type = db.Column(db.Enum(TagType), nullable=False) diff --git a/timApp/item/taskblock.py b/timApp/item/taskblock.py index 07a8b9796f..8fd9f3e20b 100644 --- a/timApp/item/taskblock.py +++ b/timApp/item/taskblock.py @@ -9,6 +9,8 @@ class TaskBlock(db.Model): __tablename__ = "taskblock" + __allow_unmapped__ = True + id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) task_id = db.Column(db.Text, primary_key=True) diff --git a/timApp/lecture/askedjson.py b/timApp/lecture/askedjson.py index 2e1bb1c105..d5c14292e6 100644 --- a/timApp/lecture/askedjson.py +++ b/timApp/lecture/askedjson.py @@ -9,6 +9,8 @@ class AskedJson(db.Model): __tablename__ = "askedjson" + __allow_unmapped__ = True + asked_json_id = db.Column(db.Integer, primary_key=True) json = db.Column(db.Text, nullable=False) hash = db.Column(db.Text, nullable=False) diff --git a/timApp/lecture/askedquestion.py b/timApp/lecture/askedquestion.py index a5936c050a..a415b4bdeb 100644 --- a/timApp/lecture/askedquestion.py +++ b/timApp/lecture/askedquestion.py @@ -15,6 +15,8 @@ class AskedQuestion(db.Model): __tablename__ = "askedquestion" + __allow_unmapped__ = True + asked_id = db.Column(db.Integer, primary_key=True) lecture_id = db.Column( db.Integer, db.ForeignKey("lecture.lecture_id"), nullable=False @@ -41,13 +43,13 @@ class AskedQuestion(db.Model): "LectureAnswer", back_populates="asked_question", overlaps="answers" ) running_question = db.relationship( - "Runningquestion", back_populates="asked_question", lazy="select", uselist=False + "Runningquestion", back_populates="asked_question", lazy="select", uselist=False, cascade_backrefs=False ) questionactivity = db.relationship( - "QuestionActivity", back_populates="asked_question", lazy="dynamic" + "QuestionActivity", back_populates="asked_question", lazy="dynamic", cascade_backrefs=False ) showpoints = db.relationship( - "Showpoints", back_populates="asked_question", lazy="select" + "Showpoints", back_populates="asked_question", lazy="select", cascade_backrefs=False ) @property diff --git a/timApp/lecture/lecture.py b/timApp/lecture/lecture.py index aa703b9fdb..3636c09a7d 100644 --- a/timApp/lecture/lecture.py +++ b/timApp/lecture/lecture.py @@ -11,6 +11,8 @@ class Lecture(db.Model): __tablename__ = "lecture" + __allow_unmapped__ = True + lecture_id = db.Column(db.Integer, primary_key=True) lecture_code = db.Column(db.Text) doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) @@ -27,11 +29,11 @@ class Lecture(db.Model): lazy="dynamic", ) asked_questions = db.relationship( - "AskedQuestion", back_populates="lecture", lazy="dynamic" + "AskedQuestion", back_populates="lecture", lazy="dynamic", cascade_backrefs=False ) messages = db.relationship("Message", back_populates="lecture", lazy="dynamic") running_questions = db.relationship( - "Runningquestion", back_populates="lecture", lazy="select" + "Runningquestion", back_populates="lecture", lazy="select", cascade_backrefs=False ) useractivity = db.relationship( "Useractivity", back_populates="lecture", lazy="select" diff --git a/timApp/lecture/lectureanswer.py b/timApp/lecture/lectureanswer.py index 093f9d3ff5..b9a3cec034 100644 --- a/timApp/lecture/lectureanswer.py +++ b/timApp/lecture/lectureanswer.py @@ -26,6 +26,8 @@ def unshuffle_lectureanswer( class LectureAnswer(db.Model): __tablename__ = "lectureanswer" + __allow_unmapped__ = True + answer_id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) question_id = db.Column( @@ -95,6 +97,6 @@ def get_totals( .filter_by(lecture_id=lecture.lecture_id) .group_by(User.id) .order_by(User.name) - .with_entities(User, func.sum(LectureAnswer.points), func.count()) + .with_only_columns(User, func.sum(LectureAnswer.points), func.count()) ) - return db.session.execute(stmt).scalars().all() + return db.session.execute(stmt).all() diff --git a/timApp/lecture/lectureusers.py b/timApp/lecture/lectureusers.py index d3d8fcd6d6..a8a7cb1d88 100644 --- a/timApp/lecture/lectureusers.py +++ b/timApp/lecture/lectureusers.py @@ -3,6 +3,8 @@ class LectureUsers(db.Model): __tablename__ = "lectureusers" + __allow_unmapped__ = True + lecture_id = db.Column( db.Integer, db.ForeignKey("lecture.lecture_id"), primary_key=True ) diff --git a/timApp/lecture/message.py b/timApp/lecture/message.py index d714f3332c..15b5d9e34e 100644 --- a/timApp/lecture/message.py +++ b/timApp/lecture/message.py @@ -5,6 +5,8 @@ class Message(db.Model): __tablename__ = "message" + __allow_unmapped__ = True + msg_id = db.Column(db.Integer, primary_key=True) lecture_id = db.Column( db.Integer, db.ForeignKey("lecture.lecture_id"), nullable=False diff --git a/timApp/lecture/question.py b/timApp/lecture/question.py index 47f8dec61c..ecc95f030b 100644 --- a/timApp/lecture/question.py +++ b/timApp/lecture/question.py @@ -3,6 +3,8 @@ class Question(db.Model): __tablename__ = "question" + __allow_unmapped__ = True + question_id = db.Column(db.Integer, primary_key=True) doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) par_id = db.Column(db.Text, nullable=False) diff --git a/timApp/lecture/questionactivity.py b/timApp/lecture/questionactivity.py index 06bc7b9d08..5b0d1c088e 100644 --- a/timApp/lecture/questionactivity.py +++ b/timApp/lecture/questionactivity.py @@ -13,6 +13,8 @@ class QuestionActivityKind(Enum): class QuestionActivity(db.Model): __tablename__ = "question_activity" + __allow_unmapped__ = True + asked_id = db.Column( db.Integer, db.ForeignKey("askedquestion.asked_id"), primary_key=True ) diff --git a/timApp/lecture/runningquestion.py b/timApp/lecture/runningquestion.py index 17bdeb622f..a2a29d6743 100644 --- a/timApp/lecture/runningquestion.py +++ b/timApp/lecture/runningquestion.py @@ -5,6 +5,8 @@ class Runningquestion(db.Model): + __allow_unmapped__ = True + asked_id = db.Column( db.Integer, db.ForeignKey("askedquestion.asked_id"), primary_key=True ) diff --git a/timApp/lecture/showpoints.py b/timApp/lecture/showpoints.py index fabb926048..811687ff6e 100644 --- a/timApp/lecture/showpoints.py +++ b/timApp/lecture/showpoints.py @@ -3,6 +3,8 @@ class Showpoints(db.Model): __tablename__ = "showpoints" + __allow_unmapped__ = True + asked_id = db.Column( db.Integer, db.ForeignKey("askedquestion.asked_id"), primary_key=True ) diff --git a/timApp/lecture/useractivity.py b/timApp/lecture/useractivity.py index 424322f52a..1150150079 100644 --- a/timApp/lecture/useractivity.py +++ b/timApp/lecture/useractivity.py @@ -5,6 +5,8 @@ class Useractivity(db.Model): __tablename__ = "useractivity" + __allow_unmapped__ = True + lecture_id = db.Column( db.Integer, db.ForeignKey("lecture.lecture_id"), primary_key=True ) diff --git a/timApp/messaging/messagelist/messagelist_models.py b/timApp/messaging/messagelist/messagelist_models.py index 0b5a342727..fe283f5e75 100644 --- a/timApp/messaging/messagelist/messagelist_models.py +++ b/timApp/messaging/messagelist/messagelist_models.py @@ -34,6 +34,7 @@ class MessageListModel(db.Model): """Database model for message lists""" __tablename__ = "messagelist" + __allow_unmapped__ = True id = db.Column(db.Integer, primary_key=True) @@ -265,6 +266,7 @@ class MessageListMember(db.Model): """Database model for members of a message list.""" __tablename__ = "messagelist_member" + __allow_unmapped__ = True id = db.Column(db.Integer, primary_key=True) @@ -506,6 +508,7 @@ class MessageListDistribution(db.Model): """Message list member's chosen distribution channels.""" __tablename__ = "messagelist_distribution" + __allow_unmapped__ = True id = db.Column(db.Integer, primary_key=True) diff --git a/timApp/messaging/timMessage/internalmessage_models.py b/timApp/messaging/timMessage/internalmessage_models.py index 24f9935e8b..3ea715c079 100644 --- a/timApp/messaging/timMessage/internalmessage_models.py +++ b/timApp/messaging/timMessage/internalmessage_models.py @@ -18,6 +18,7 @@ class InternalMessage(db.Model): """A TIM message.""" __tablename__ = "internalmessage" + __allow_unmapped__ = True id = db.Column(db.Integer, primary_key=True) """Message identifier.""" @@ -70,6 +71,7 @@ class InternalMessageDisplay(db.Model): """Where and for whom a TIM message is displayed.""" __tablename__ = "internalmessage_display" + __allow_unmapped__ = True id = db.Column(db.Integer, primary_key=True) """Message display identifier.""" @@ -105,6 +107,7 @@ class InternalMessageReadReceipt(db.Model): """Metadata about read receipts.""" __tablename__ = "internalmessage_readreceipt" + __allow_unmapped__ = True message_id = db.Column( db.Integer, db.ForeignKey("internalmessage.id"), primary_key=True diff --git a/timApp/messaging/timMessage/routes.py b/timApp/messaging/timMessage/routes.py index 15a0ffff9e..00541d860e 100644 --- a/timApp/messaging/timMessage/routes.py +++ b/timApp/messaging/timMessage/routes.py @@ -379,11 +379,11 @@ def send_message_or_reply(message: MessageBody, options: MessageOptions) -> Resp ) recipients = get_recipient_users(message.recipients) message_doc = create_tim_message(tim_message, options, message, recipients) - db.session.add(tim_message) pages = get_display_pages(options.pageList.splitlines()) create_message_displays(tim_message, pages, recipients) + db.session.add(tim_message) db.session.commit() return json_response({"docPath": message_doc.path}) diff --git a/timApp/note/notes.py b/timApp/note/notes.py index 7208c8074c..00221a64cf 100644 --- a/timApp/note/notes.py +++ b/timApp/note/notes.py @@ -67,7 +67,7 @@ def get_notes( .filter(f) .order_by(UserNote.id) ) - return process_notes(db.session.execute(stmt).scalars().all()) + return process_notes(db.session.execute(stmt).all()) def move_notes(src_par: DocParagraph, dest_par: DocParagraph): diff --git a/timApp/note/usernote.py b/timApp/note/usernote.py index 4f818b4bfb..3502223a02 100644 --- a/timApp/note/usernote.py +++ b/timApp/note/usernote.py @@ -7,6 +7,8 @@ class UserNote(db.Model): """A comment/note that has been posted in a document paragraph.""" __tablename__ = "usernotes" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) """Comment id.""" diff --git a/timApp/notification/notification.py b/timApp/notification/notification.py index bebac6b678..a56a0296f2 100644 --- a/timApp/notification/notification.py +++ b/timApp/notification/notification.py @@ -29,6 +29,8 @@ class Notification(db.Model): """Notification settings for a User for a block.""" __tablename__ = "notification" + __allow_unmapped__ = True + user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) """User id.""" diff --git a/timApp/notification/pending_notification.py b/timApp/notification/pending_notification.py index a3402dbfe3..7935d93b0d 100644 --- a/timApp/notification/pending_notification.py +++ b/timApp/notification/pending_notification.py @@ -10,6 +10,7 @@ class PendingNotification(db.Model): __tablename__ = "pendingnotification" + __allow_unmapped__ = True id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) diff --git a/timApp/peerreview/peerreview.py b/timApp/peerreview/peerreview.py index fe5b1850dd..00558bc6b3 100644 --- a/timApp/peerreview/peerreview.py +++ b/timApp/peerreview/peerreview.py @@ -7,6 +7,8 @@ class PeerReview(db.Model): """A peer review to a task.""" __tablename__ = "peer_review" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) """Review identifier.""" diff --git a/timApp/peerreview/util/groups.py b/timApp/peerreview/util/groups.py index dd4f39a272..0ab0fc10d9 100644 --- a/timApp/peerreview/util/groups.py +++ b/timApp/peerreview/util/groups.py @@ -24,7 +24,7 @@ def generate_review_groups(doc: DocInfo, task_ids: list[TaskId]) -> None: if user_groups: user_ids = [ uid - for uid, in db.session.execute( + for uid in db.session.execute( select(UserGroupMember.user_id) .join(UserGroup, UserGroupMember.group) .filter(membership_current & (UserGroup.name.in_(user_groups))) diff --git a/timApp/plugin/calendar/calendar.py b/timApp/plugin/calendar/calendar.py index a1eae8ad09..410da2e47a 100644 --- a/timApp/plugin/calendar/calendar.py +++ b/timApp/plugin/calendar/calendar.py @@ -478,7 +478,7 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev """ filter_opts = filter_opts or FilterOptions() - stmt = select(Event) + stmt = select(Event.event_id) event_queries = [] event_filter = false() @@ -506,7 +506,7 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev UserGroup.name.in_(filter_opts.groups) ) # noinspection PyUnresolvedReferences - event_filter |= Event.event_id.in_(subquery_event_groups.subquery()) + event_filter |= Event.event_id.in_(subquery_event_groups) # Filter out any tags and groups if filter_opts.tags is not None: @@ -518,7 +518,7 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev if filter_opts.showImportant: # noinspection PyUnresolvedReferences - important_q = select(Event).filter( + important_q = select(Event.event_id).filter( Event.event_id.in_(subquery_event_groups_all) & Event.important.is_(True) ) event_queries.append(important_q) @@ -529,10 +529,11 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev u.get_groups(include_expired=False) .join(Enrollment, Enrollment.usergroup_id == UserGroup.id) .with_only_columns(Enrollment.event_id) - .subquery() ) # noinspection PyUnresolvedReferences - booked_query = select(Event).filter(Event.event_id.in_(enrolled_subquery)) + booked_query = select(Event.event_id).filter( + Event.event_id.in_(enrolled_subquery) + ) event_queries.append(booked_query) if filter_opts.showBookedByMin is not None: @@ -546,7 +547,7 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev ) ).subquery() booked_min_query = ( - select(Event) + select(Event.event_id) .select_from(booked_min_subquery) .join(Event, Event.event_id == booked_min_subquery.c.event_id) .filter(booked_min_subquery.c.count >= filter_opts.showBookedByMin) @@ -562,6 +563,9 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev if event_queries: stmt = stmt.union(*event_queries) + stmt = select(Event).filter(Event.event_id.in_(stmt)) + else: + stmt = stmt.with_only_columns(Event) stmt = stmt.filter(timing_filter) return db.session.execute(stmt).scalars().all() diff --git a/timApp/plugin/calendar/models.py b/timApp/plugin/calendar/models.py index 5127a47890..4a9d4c3aca 100644 --- a/timApp/plugin/calendar/models.py +++ b/timApp/plugin/calendar/models.py @@ -27,6 +27,8 @@ class EventGroup(db.Model): """Information about a user group participating in an event.""" __tablename__ = "eventgroup" + __allow_unmapped__ = True + event_id = db.Column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) """Event the the group belongs to""" @@ -49,6 +51,8 @@ class Enrollment(db.Model): """A single enrollment in an event""" __tablename__ = "enrollment" + __allow_unmapped__ = True + event_id = db.Column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) """Event the enrollment is for""" @@ -95,6 +99,8 @@ class EventTagAttachment(db.Model): """Attachment information for the event tag""" __tablename__ = "eventtagattachment" + __allow_unmapped__ = True + event_id = db.Column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) """Event the tag is attached to""" tag_id = db.Column(db.Integer, db.ForeignKey("eventtag.tag_id"), primary_key=True) @@ -105,6 +111,8 @@ class EventTag(db.Model): """A string tag that can be attached to an event""" __tablename__ = "eventtag" + __allow_unmapped__ = True + tag_id = db.Column(db.Integer, primary_key=True) """The id of the tag""" @@ -172,6 +180,8 @@ class Event(db.Model): """A calendar event. Event has metadata (title, time, location) and various participating user groups.""" __tablename__ = "event" + __allow_unmapped__ = True + event_id = db.Column(db.Integer, primary_key=True) """Identification number of the event""" @@ -412,6 +422,8 @@ class EnrollmentType(db.Model): """Table for enrollment type, combines enrollment type ID to specific enrollment type""" __tablename__ = "enrollmenttype" + __allow_unmapped__ = True + enroll_type_id = db.Column(db.Integer, primary_key=True) """Enrollment type""" @@ -423,6 +435,8 @@ class ExportedCalendar(db.Model): """Information about exported calendars""" __tablename__ = "exportedcalendar" + __allow_unmapped__ = True + user_id = db.Column( db.Integer, db.ForeignKey("useraccount.id"), primary_key=True, nullable=False ) diff --git a/timApp/plugin/importdata/importData.py b/timApp/plugin/importdata/importData.py index 309bc3ca3a..9c73fdf41a 100644 --- a/timApp/plugin/importdata/importData.py +++ b/timApp/plugin/importdata/importData.py @@ -384,7 +384,7 @@ def answer(args: ImportDataAnswerModel) -> PluginAnswerResp: stmt = select(User).filter(User.id.in_([int(i) for i in idents])) except ValueError as e: return args.make_answer_error(f"User ids must be ints ({e})") - users = {str(u.id): u for u in db.sesion.execute(stmt).scalars()} + users = {str(u.id): u for u in db.session.execute(stmt).scalars()} elif id_prop == "email": stmt = select(User).filter(User.email.in_(idents)) users = {u.email: u for u in db.session.execute(stmt).scalars()} diff --git a/timApp/plugin/jsrunner/util.py b/timApp/plugin/jsrunner/util.py index 76dedd0253..7e3c98fae1 100644 --- a/timApp/plugin/jsrunner/util.py +++ b/timApp/plugin/jsrunner/util.py @@ -470,12 +470,12 @@ def save_fields( valid=True, saver=curr_user, ) + db.session.add(ans) saveresult.fields_changed += 1 # If this was a global task, add it to all users in the answer map so we won't save it multiple times. if task_id.is_global: for uid in user_map.keys(): answer_map[uid][ans.task_id] = ans - db.session.add(ans) return saveresult diff --git a/timApp/plugin/pluginControl.py b/timApp/plugin/pluginControl.py index 38c24f6728..47ad9b4562 100644 --- a/timApp/plugin/pluginControl.py +++ b/timApp/plugin/pluginControl.py @@ -384,7 +384,7 @@ def get_answers(user, task_ids, answer_map): sub = ( valid_answers_query(task_ids) .add_columns(col, cnt) - .with_entities(col, cnt) + .with_only_columns(col, cnt) .group_by(Answer.task_id) .subquery() ) diff --git a/timApp/plugin/plugintype.py b/timApp/plugin/plugintype.py index d8315281b5..6e06c657a4 100644 --- a/timApp/plugin/plugintype.py +++ b/timApp/plugin/plugintype.py @@ -35,6 +35,8 @@ def to_json(self) -> dict[str, Any]: # TODO: Right now values are added dynamically to the table when saving answers. Instead add them on TIM start. class PluginType(db.Model, PluginTypeBase): __tablename__ = "plugintype" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) type = db.Column(db.Text, nullable=False, unique=True) @@ -53,7 +55,7 @@ def resolve(p_type: str) -> "PluginType": # Use a lock to prevent concurrent access with filelock.FileLock("/tmp/plugin_type_create.lock"): try: - tmp_session = db.create_session({}) + tmp_session = db._make_session_factory({}) session = tmp_session() session.add(PluginType(type=p_type)) session.commit() diff --git a/timApp/plugin/timtable/row_owner_info.py b/timApp/plugin/timtable/row_owner_info.py index 4b9598a9e0..9c0e5306cc 100644 --- a/timApp/plugin/timtable/row_owner_info.py +++ b/timApp/plugin/timtable/row_owner_info.py @@ -8,6 +8,8 @@ class RowOwnerInfo(db.Model): """ __tablename__ = "rowownerinfo" + __allow_unmapped__ = True + doc_id = db.Column(db.Integer, primary_key=True) par_id = db.Column(db.Text, primary_key=True) unique_row_id = db.Column(db.Integer, primary_key=True) diff --git a/timApp/printing/documentprinter.py b/timApp/printing/documentprinter.py index c7d19f70bf..c8f57ed7ed 100644 --- a/timApp/printing/documentprinter.py +++ b/timApp/printing/documentprinter.py @@ -11,6 +11,7 @@ from flask import current_app from pypandoc import _as_unicode, _validate_formats from pypandoc.py3compat import string_types, cast_bytes +from sqlalchemy import select from timApp.auth.accesshelper import has_view_access from timApp.auth.sessioninfo import get_current_user_object @@ -49,6 +50,7 @@ from timApp.printing.printeddoc import PrintedDoc from timApp.printing.printsettings import PrintFormat from timApp.timdb.dbaccess import get_files_path +from timApp.timdb.sqa import db from timApp.user.user import User from timApp.util.utils import cache_folder_path from tim_common.html_sanitize import sanitize_html @@ -862,7 +864,6 @@ def get_printed_document_path_from_db( plugins_user_print: bool = False, url_macros: dict[str, str] | None = None, ) -> str | None: - # noinspection PyUnresolvedReferences existing_print: PrintedDoc | None = ( db.session.execute( select(PrintedDoc) @@ -1053,8 +1054,7 @@ def tim_convert_input( stderr = _decode_result(stderr) if stdout or stderr: raise RuntimeError( - 'Pandoc died with exitcode "%s" during conversion. \nSource=\n%s' - % (stdout + stderr, number_lines(source)) + f'Pandoc died with error "{stderr}" during conversion.\nOutput: {stdout}.\nSource=\n{number_lines(source)}' ) with open(latex_file, encoding="utf-8") as r: diff --git a/timApp/printing/pandoc_imagefilepathsfilter.py b/timApp/printing/pandoc_imagefilepathsfilter.py index 96a99138ef..e490764b06 100755 --- a/timApp/printing/pandoc_imagefilepathsfilter.py +++ b/timApp/printing/pandoc_imagefilepathsfilter.py @@ -14,7 +14,6 @@ TODO: BETTER DOCUMENTATION """ -import imghdr import os import re import tempfile @@ -25,6 +24,7 @@ from timApp.defaultconfig import FILES_PATH from timApp.document.randutils import hashfunc +from timApp.util.file_utils import guess_image_type APP_ROOT = "/service/timApp" @@ -233,7 +233,7 @@ def handle_images(key, value, fmt, meta): # open("Output.txt", "a").write("retrieve: " + url + " -> " + img_dl_path + "\n") urllib.request.urlretrieve(url, img_dl_path) if not ext: - img_type = imghdr.what(img_dl_path) + img_type = guess_image_type(img_dl_path) if img_type: img_dl_path_ext = f"{img_dl_path}.{img_type}" # open("Output.txt", "a").write("img_dl_path_ext = " + img_dl_path_ext + "\n") diff --git a/timApp/printing/printeddoc.py b/timApp/printing/printeddoc.py index 979de1f53c..2902acd799 100644 --- a/timApp/printing/printeddoc.py +++ b/timApp/printing/printeddoc.py @@ -8,6 +8,8 @@ class PrintedDoc(db.Model): (CSS printing does not count because it happens entirely in browser).""" __tablename__ = "printed_doc" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) """Id of the printed document.""" diff --git a/timApp/readmark/readparagraph.py b/timApp/readmark/readparagraph.py index e96cd91167..cc9864fc2a 100644 --- a/timApp/readmark/readparagraph.py +++ b/timApp/readmark/readparagraph.py @@ -8,6 +8,8 @@ class ReadParagraph(db.Model): """Denotes that a User(Group) has read a specific paragraph in some way.""" __tablename__ = "readparagraph" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) """Readmark id.""" @@ -37,3 +39,5 @@ class ReadParagraph(db.Model): ) usergroup = db.relationship("UserGroup", back_populates="readparagraphs") + + diff --git a/timApp/readmark/routes.py b/timApp/readmark/routes.py index 315c0c2b97..fa6bfe2e2e 100644 --- a/timApp/readmark/routes.py +++ b/timApp/readmark/routes.py @@ -294,8 +294,8 @@ def row_to_dict(row): def gen_rows(): yield column_names - yield from (maybe_hide_name_from_row(row) for row in stmt) + yield from (maybe_hide_name_from_row(row) for row in db.session.execute(stmt)) return csv_response(gen_rows(), dialect=csv_dialect) else: - return json_response(list(map(row_to_dict, stmt.all()))) + return json_response(list(map(row_to_dict, db.session.execute(stmt).all()))) diff --git a/timApp/scheduling/scheduling_routes.py b/timApp/scheduling/scheduling_routes.py index 262d3f5511..70d4546da1 100644 --- a/timApp/scheduling/scheduling_routes.py +++ b/timApp/scheduling/scheduling_routes.py @@ -80,7 +80,7 @@ def get_scheduled_functions(all_users: bool = False) -> Response: if not all_users: stmt = stmt.filter( - BlockAccess.block_id.in_(get_owned_objects_query(u).subquery()) + BlockAccess.block_id.in_(get_owned_objects_query(u)) ) scheduled_fns: list[PeriodicTask] = db.session.execute(stmt).scalars().all() diff --git a/timApp/sisu/scim.py b/timApp/sisu/scim.py index 38640e6f52..0d0c29526c 100644 --- a/timApp/sisu/scim.py +++ b/timApp/sisu/scim.py @@ -133,7 +133,7 @@ class SCIMGroupModel(SCIMCommonModel): SCIMGroupModelSchema = class_schema(SCIMGroupModel) -@dataclass(frozen=True) +@dataclass class SCIMException(Exception): code: int msg: str diff --git a/timApp/sisu/scimusergroup.py b/timApp/sisu/scimusergroup.py index ebef807d30..323a01d8c0 100644 --- a/timApp/sisu/scimusergroup.py +++ b/timApp/sisu/scimusergroup.py @@ -10,6 +10,8 @@ class ScimUserGroup(db.Model): __tablename__ = "scimusergroup" + __allow_unmapped__ = True + group_id = db.Column(db.Integer, db.ForeignKey("usergroup.id"), primary_key=True) external_id = db.Column(db.Text, unique=True, nullable=False) diff --git a/timApp/slide/slidestatus.py b/timApp/slide/slidestatus.py index 93b666dd17..520e917ec7 100644 --- a/timApp/slide/slidestatus.py +++ b/timApp/slide/slidestatus.py @@ -3,6 +3,8 @@ class SlideStatus(db.Model): __tablename__ = "slide_status" + __allow_unmapped__ = True + doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) status = db.Column(db.Text, nullable=False) diff --git a/timApp/tests/browser/browsertest.py b/timApp/tests/browser/browsertest.py index 2302c7c154..a05d5161ed 100644 --- a/timApp/tests/browser/browsertest.py +++ b/timApp/tests/browser/browsertest.py @@ -10,7 +10,10 @@ from urllib.parse import urlencode import requests +from flask.testing import FlaskClient +from flask_testing import LiveServerTestCase from selenium import webdriver +from selenium.common import WebDriverException from selenium.common.exceptions import ( NoSuchElementException, StaleElementReferenceException, @@ -31,8 +34,7 @@ TEST_USER_2_NAME, TEST_USER_3_NAME, ) -from timApp.tests.server.timroutetest import TimRouteTest -from timApp.tests.timliveserver import TimLiveServer +from timApp.tests.server.timroutetest import TimRouteTestBase from timApp.timdb.sqa import db from timApp.user.user import Consent @@ -50,7 +52,7 @@ options.add_argument("--disable-dev-shm-usage") -class BrowserTest(TimLiveServer, TimRouteTest): +class BrowserTest(LiveServerTestCase, TimRouteTestBase): login_dropdown_path = "//tim-login-menu/tim-user-menu/div/button" screenshot_dir = "/service/screenshots" @@ -60,8 +62,16 @@ def __init__(self, *args, **kwargs): def get_screenshot_tolerance(self) -> float: return 5 + def create_app(self): + from timApp.tim_app import app + + return app + + def _init_client(self) -> FlaskClient: + return self.app.test_client() + def setUp(self): - TimLiveServer.setUp(self) + TimRouteTestBase.setUp(self) self.drv = webdriver.Chrome(options=options) # Some CI browser tests run slower and can cause render timeouts without a longer script timeout self.drv.implicitly_wait(10) @@ -76,7 +86,7 @@ def login_browser_as(self, email: str, password: str, name: str) -> None: :param password: User password :param name: User's full name. Used to test that the user is logged in properly. """ - self.client.__exit__(None, None, None) + # self.client.__exit__(None, None, None) self.goto("") elem = self.drv.find_element(By.XPATH, "//tim-login-menu/button") elem.click() @@ -94,7 +104,7 @@ def login_browser_as(self, email: str, password: str, name: str) -> None: self.wait.until( ec.text_to_be_present_in_element((By.XPATH, self.login_dropdown_path), name) ) - self.client.__enter__() + # self.client.__enter__() @contextmanager def temp_config(self, settings: dict[str, Any]): @@ -333,7 +343,7 @@ def tearDown(self): self.save_screenshot(scn_path) except Exception as e: warnings.warn(f"Failed to save screenshot to {scn_path}: {e}") - TimLiveServer.tearDown(self) + TimRouteTestBase.tearDown(self) self.drv.quit() def goto_document(self, d: DocInfo, view="view", query=None): @@ -472,10 +482,9 @@ def login( :return: Response as a JSON dict. """ - if self.client.application.got_first_request: - with self.client.session_transaction() as s: - s.pop("last_doc", None) - s.pop("came_from", None) + with self.client.session_transaction() as s: + s.pop("last_doc", None) + s.pop("came_from", None) return self.post( "/emailLogin", data={"email": email, "password": passw, "add_user": add}, diff --git a/timApp/tests/browser/expected_screenshots/csplugin/python_after_answer.png b/timApp/tests/browser/expected_screenshots/csplugin/python_after_answer.png index 09ec9d01f33028427434f1eadb740cdbe5f96bd7..acb318810608b7679363ba7eecf9bc1c042132d5 100644 GIT binary patch literal 9168 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ͅ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 literal 9063 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 WDaxH~Gm--DATSNRy9Ku&zWg6Hph&*} diff --git a/timApp/tests/browser/expected_screenshots/csplugin/python_after_answer_switch.png b/timApp/tests/browser/expected_screenshots/csplugin/python_after_answer_switch.png index 5fccd1235424e443b6012f16830b43601bd22a4e..a806d2a598646b4e1d49bfea402a612138ed8cae 100644 GIT binary patch literal 7456 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 literal 7333 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 diff --git a/timApp/tests/browser/expected_screenshots/csplugin/python_before_answer.png b/timApp/tests/browser/expected_screenshots/csplugin/python_before_answer.png index 88609fe7c5a7d2bc473e2e6aaf175bd0979ed460..86ea5b561968a188ec22344bc85420b1363da59c 100644 GIT binary patch literal 7456 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 literal 7333 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 QCxuOp`E};s$%TBX0O({C0ssI2 delta 78 zcmbOxJ56>22Qw!JGrO{`bY_IgWa3 QNnw-QAQfOaxsXp40J*Rdc>n+a diff --git a/timApp/tests/browser/expected_screenshots/imagex/canvas_legacy_init.png b/timApp/tests/browser/expected_screenshots/imagex/canvas_legacy_init.png index 07548e355f010216120b671fa044d897f1ad56d6..5e2a9e624014478e6218dbe46603713456b7b732 100644 GIT binary patch delta 99 zcmZ24wO(q&e22Qw$T6t6JbDc8BJn+2IAxW&zNjm$y}jjaqVtqjbx4GgRd4BCEv4Vvu0 QCxuP!`x4)GlMDG&0TZ|tV*mgE delta 78 zcmbOxJ56>22Qw!JGrO`Oe+EnTWljp#tD_2>{7hKD)Op#5;ry}dy1Ru8kvO{8e17!S{a*Z8yHv_7;uz*+BkWS QrxZ51N^LLR$!|SX0b&^yumAu6 delta 78 zcmbOljp#tD_29L(%0g4OPuQZ_azdx~4=8W@Kdm|Gc}Ss9vZ8yHv_7<}dTIX!ug RrxZ51mY^@?lizx(0swbZ7b5@w diff --git a/timApp/tests/browser/expected_screenshots/jsrunner/area_visibility_no_click_refresh.png b/timApp/tests/browser/expected_screenshots/jsrunner/area_visibility_no_click_refresh.png index c71ef28d79a94ecdb6117d30c432caf9ccf5af5b..971545b0eb5833c3d2e31d941d0ee47667d15ad9 100644 GIT binary patch delta 97 zcmZ3Rx;Aw}B`3QSudr%{(wpxaoAw!cnClvug%}!J8CqHyn`;{wSQ!|sWBkd=z`&qd k;u=wsl30>zm7AZEnO4bQWME{hYk)<={TRoclRJ!50lrNf*Z=?k delta 97 zcmZ3Rx;Aw}B_{_nyNY17`=*qQP5TTzEOZTwLk!HVjLobJ&9w~-tPBjk^81`-U|>)! kag8WRNi0dV%FR#7OsixtGB7gMHNc{wCFo1}{7hKvJMI%nH!rPg^Qc(8kvO{8e17!S{Yet8yHv_7#K}yIySjF QLJFIl^GU0!$!j810jXaX8~^|S delta 78 zcmcb(k@4b2#tD_29L(%0TpyLHJ2o~w3KzG~H82h_Ft;)`vobW$HZZU8kvO{8e17!S{WN?8yHv_7-TOy-aR?n QNeY{s)bD!p$#a}k0kQ2DKmY&$ delta 78 zcmX@u%6PDqaY7|02Q#}0cblrW+{UI$j^Y-&2F4)<=2pgLR)&V!1_o9J2Gj0Wew>`` QB!x}xdDB7P$#a}k0gnb3egFUf diff --git a/timApp/tests/browser/expected_screenshots/pareditor/ace_hello_world_3.png b/timApp/tests/browser/expected_screenshots/pareditor/ace_hello_world_3.png index 8b3398833f5b208e86ccd5d1628e6c72936a251e..db5642658f345411bf727b7ac445250e63756e2a 100644 GIT binary patch delta 99 zcmeC1%+xoTX+k9@yA-d8Alvo6){RZ`Iy}sEjm$y}jjfCftPIVy4GgRd3_34w-ps(j mpjzS@QIe8al4_NkpOTqY$zWt)WUOm|MZ=FiZvM#`ooWE)B^^Zo delta 99 zcmeC1%+xoTX+k9@2Q#}WWBu#NjvJfib$D3l8W@Kdm|Gc}TNxN?8yHv_7;N1x6v)89 npjzS@QIe8al4_NkpOTqY$zWt)WUOm|MZ=L-tKLq|=u`s$%+ww# diff --git a/timApp/tests/browser/expected_screenshots/pareditor/autocomplete.png b/timApp/tests/browser/expected_screenshots/pareditor/autocomplete.png index c28535bd935eec8265066472de1e06c673f1bda8..c3bfd93c52735e9c973a775fe5429e6a4eccf6fc 100644 GIT binary patch delta 99 zcmX@z$8@@nX+k9@yA-d8DB~`#IUAdz+dRy5jm$y}jjfCftc(n`4GgRd3{rX;vlti{ mR7+eVN>UO_Qmu0HQ!>*k8H@~!jCBpLXb`<{7fU67}3>iW{34E5*%qjm$y}jjfCftc*;w4GgRd49vEyJ3Dz+ Rr4%+f_m^UYlTTNw0RXx57nT43 delta 78 zcmezPnepRi#tD_29L(&h9Bk&=Pd7F(R*GBb8W@Kdm|Gc}TNzkr8yHv_7%1q6H%*>Z QDTPfgu;{(r diff --git a/timApp/tests/browser/expected_screenshots/pareditor/textarea_hello_world.png b/timApp/tests/browser/expected_screenshots/pareditor/textarea_hello_world.png index 5a5eef9f38c592207872fe531a532cfbe21bede1..3044e02144374b09fe714c48f9288bf935a30761 100644 GIT binary patch literal 35196 zcmeFZcU03`w>HYQVMBB)BHcm}5KyED2y8{b&|Bz5ksj#~Bs7&RY(+prliq7csG$c` z1f=b3Z8Hqg^#JIQ^L zg@uI;^6-HX3(Mh`EG&m?jvoW>IA83P0xw6L@9ExSVX25cwfFca`2F&eheoU>&STu)Em-U?}~ zQ$;DjNjKv0rQ+h^H?gs1PEG|rzP_)h{e69U;LqC6pYKysQ?cEDfMGpS)9LK&)X>p+ zySKMDx3nY{6chv_I-K|M@rh4NEU0qsFR`xo@d*g1g+svOEKmPd3l45#KKt$4W18jD zH#i)wzJGRh_5@fz78Z|7jiseGB7`lOJycd!mfA2El!eDWDK%AGNl6Jtd|b7>yj&*< z=FjqxP5sc3-yf@;{qMc{8Xx(j;7eHuXIHttNiWFDE3#_|RrFtq$EKRw=L^m+G{-3U zFFk9(YJXyTm+Z>?l>>1dKc4^bn7eIe)+ZdCv%A45BvfNHTGfLlOuA_{6p&=KIa{me zvZPsvCgt(0AXaXld(72PHmldx;b_&E+!nKS;$v_3H%qhf$F0Y7Gwxb2cDKyRY;iXz zxb;4zlP69X_Gc)Ux$>kd1(3`k7 zwbll?3!hJXAhEkrdY?W2x=6>nu&gRYzfI%XDMv@ML#h|WIzudYL-)RwI_aMf_X|DH zJT&5*74~91Cs&%@Qyz}+GbtaRrol2YE@h%`#V!*Mv$K}0LmUQ}c-^%SIj4 z9sj#F`s~VC{mmul=a;*mwC!1(8^eqQ=YWJG;AfyIT8Lht5Cg%VWkbUc9KR ztu;$m^nZi1g?05N2;UVbIhkNXUz)(rnT;FX8ca@+SGGRi5wR3EhIMonVf@g|L(ruH zx|=`M6d=cITW&-X-s<-+tnla4XWsp#pqS$4V)v#w{XAwi+m(CWULBV7K_5cv{yvml zmFeM0k$}93>Kv+bj#)^kMWsylAP^j(|4175L0U7#%3K99jc^(nVm1Urq>THTD-`|B zH0ShY0v)Ekfidj2E zPfs{7kh8nRkL7vv^~3}2{r&xu$Bz%BDfn*et@m@*>3DcxDE*mbbShC?LBZmWKmOpp z`>cG4fJX5t!I|jZnDYDuy zA|@d+GV<8zOIbqNiA4dcQ_OY#K07N5Bix`()aqMUSd>^)6-2P{6jVBPyM6xqgreWV zJC(g3YTyZ-Se2b3{=m6&xP$%O5`*lQ_V)HNz7!okMZbqJF)={+2SL|!WOBayM2%}- zn!?gVZJ8;?y4ZKVuh@s&#eLi9t+kB}nsGO!Ev?t&fq6uGv#mN=oI=l!OhUE4MHlt* zd{0PzkE*p(E(zZ?+Svbl)@_QQ`OR6{TbJ~Bhf*%DQ!EsSEXfV zhldq|FRRxpM5NS;=3mi2(mj@cBtvh9NQgd>LLcKIsowbu?76A+)5H>!fMH`PGbAcC z9h?MN`s}b`wUkA~6@2Mzc1LFMqtGXeS@yvl|G;YdKBtnmEmaYB&y^ml4bnnp@OT@b z$CHLKTiT273vmhwIazJc;-sLJ@O2?_x=_ll;|Km{cm4`tWo&)YBD-Nqj7ZR^c}-DS zA+s|v!jJ=%{>(JO&jFdEM--@IM6uMZLRr-Jjn=*E>Y7ep8{=~k8=@!=+`0-&in5T7 zXy=%gUf1Ba!=fql5aqVaZXmCEuvLtpqa)YJ;#tf&eY2aH=`nZWvR&i!sWZ(EfxCA~ z<{9LX?WrLIZS30ij?dPnU~eV|W>{p1yyQ_9v=fh4J_tqH4Ag9`Xc5w+`k6v$ZM}?d zijL*y<9O;(h^?=m=#ivD>L) zmY;T41uzeMy>Y8d$YY!J>aBKxBs(rmWkoS_WVKJ=+lzg#4NOfjE6cW)i>~!uy|*@g zRdL8r*Kfax?i*U46^He4KGxFeNi0kXhp#>~vcF$5P9mdTsIA8Y3WcaD^mmS72A($t zhP*5Y_A>HK8cFhV>{v?Zep2mlFV6_+J5rEa_V!f9o8_l!w^fyLb*bMIGE#E0-bf;T zRXoDGFFdBL)YTZjvU-N=+FTP@@YvvTon=4g1aFpZw8JZw^D!@zpoe6_O=0 zjbhvg@A(Jst&vd%k27M5s)8!NGEX--Q^ ztA-IJ*VmNb%kO|eUc#fCD>Cdtx78l8GF{{ng@J}s>@ zR(0PTJeHr9p3c+xbxfhR71F*AM{2H{=SeG}C?lx)0+2+-0Ek*lrgOVc4{ia!y8_!dTVPITNG zZb22gP7~tnTs3<$d^C)vQU?VnnoEmj;?&ekZnuFSf8pp|hUyW{bLfdrdDCGr`0`ty zxVNoe1&3G}Esr9vx4v6hObxr_7d^g$_!%wc<9o2`RqkU^o$)L?$LU5%p;2gzYgl(qe>}g zmn`BGsvt8l4ZmbJdy=%tS|&iWA!$*m9BlNdq$`TSH`QAh7jjeE(xg87Ej>t?+HNYK zNV;dG_}`WJwtcOum*JWGO+Ufxfq4V4df8k+GyPL2{k=4i4sV+Liq_ zmJ1<48%xHK^xhsXzNt*3g(sF+seE5=3&nB~3*-f^jBH>GMoMsGxYbx*k;T6VoOAkd~G0zqVr0;zcDjgV%If_SN zM@Dhhe8)VSu@&9;?aH#LCA6fmt545ua?%PoHEz8nch(zV#4Q8h7g2622^pF^i0Il; z;@IwSpAXU!ugi(LivxxS9jll5m5O@)h_m+>2E#4Cl^C82@#lVa{pP5;X6#;oFBd1L zl0OZqF_ph|jQ8%Q-_huFb7+QbpmPh;`s7BgC#w2fRzK%FggaLi8Oiwpy&WTc-Hg4x#keY()#l&Z%Tw|AF9h;c9iIsEJ`ln&@ z#{M@dQ4OA$jc;i$na*O_PK3wD#X;Eb@@D<=9UE^^8f!{RZNxE;eDi^#(G`CZJ_j9( z)t0IpUAQ4(d$t%6efQMeYrdjZa)L#c|D>kWti6wlN?#G+y=+GOY!|-&qOT!0aO&pd z%o|BD@_f;4kzvmrM&4yJpVFx&6))U+8F7@jI8fB9UPU!z}<#B%|LH+RvxsHJ4X`&25m^%-Jb^Yw4gTlimFUG zbX1ZcwsAr~&KYm?sJtpIO=oGKO}`VO@aD}|1HG7P`sy=MP?$n}&}esAb#BA7na`^F zO;5`YJ5%+;-{(OL^kJTpZtj<@N7CwUYts{s^E2BTVy?vZR#z4wwF+B_4Ms&?7g**19{AtXloBRz%5GA?`&RM}0#O&kt7Yhc z*$=svQrrCX$LToS3rZzH{SY+_Xxz z&?)PYqSv+K(`3~1)Hy_J-RKSA8>alY@6sITeC@U%Lte&nTD5aclAP7LYaAc{;hKIK za$8avI3DcqT}`CJ9CugOZ>KsM@vW;&KRT+j@>w$&uom>BK*4IjpoNj9W}Nix=u|h= zu?hvlkk!d#&z(|%C9A!V`X}^P_Bpqqn>km4?vW2*K^>TU`c}#_Hd?JF+v*RK*+dn$ z!OiyRzfpB{4PzEfD!~k14$jmACqKh;Tw->eZIyOnbuM(zK!HOP2OeNL?<>^`i-D2t zZ27+J(NX(3Um2e&9uioO=pT(i=O}vhMzVeE^LhetnDV zU8mwvB5|F0Ab1zQc1_L0!vh2_qIE8jFT49XI|rFQl14PQ>)KqWkH0^jfVQG<&u7{h zH}*y1*IV^HFLI#jO6)nz{HZn?h57~5m+TJ zPyX5%>J*jI3Bi_z8p_$_STEJ1p#3&>ePy{e6MtgepX5v-wu=;}tu%n=0BlR1rsmPq zvKQk2;M6*&r4GGJ?GjB=wF)R-pT1Ka0($I*ZQ4iD)OL-kzwcHdR=dyk z?k&Fsae-EUjQA8!*#)~)ot;OPQ{E&8WU%%;X)dowl2^s;5+|(+sgtDmE#!6YhDxg%wTc&p}2q@er_Sj^uh{*#++NZ+!@TpAhoWns#Y>06VMVD>3 zUpt3Rnd26s=(Wz5k#+QY!)`*fdgNLX^O{%!vL`3^Xf5%jrA_X^+k>L&ubdZpJ7wq0 z+6{|E6}bKSJ$y5@D$PBeG&=cYXJ@HJCle&pR*Pp3XxEaIE^_!4NbSC4=kI41z;#aD z|3fZ&l${v_n{+2iFn!;skMM=gW8R&CN}>p}f~Mp3|7#RC$zo z)WxICfm>^9MkXfl+p8ZwT*@z?u-%S|j)nrqx&cmeYktw)HX-^J=(#`t{4@T;2bYYC z7cUAcDK(}pQH9`7^+o^=F_+43{Yl+pWm8L6^qGrBuCT3-pA1kM6(^MvFv1ccYg>lz zs({mc!FRJoUw?SmH{nv4Z%vj?dY!TX9iOpIsUhSP8e zL@q*_B1#fzJnucHK1Ir{;bmiy!OtFQo*r+x(>A+e#E6Q6`W~w zW@!|!FNHHy5Ac^ltA;i!VDLXh$(K3Jl%nKmNI$u?7Q^GN#q}wBn^>J@M zJxZHQDPlwQTQu$+O4bnuk+JyYuNJ^kCY^o}qX+NmsSeIo*L3_4wk1g}D*ESOU+y$H z2z{}ww>q<~ZoK%uV^XjxCu^m+f@#v4Z}n)QphZ)`@UTz9`}e4Ey0VPRv#hDii#gX= zjr`UY61M1cqlVJBv@~7Ca-z=%dp*6Wv9(kE=8jI>ZyIMx7skH!1UpCNHw_-;jJP7Y z%DwnJtpC2icv8-0I-^HkRWW#`<=an{d|n4>i)PbSz3Hau3J?8mc`?B(2U_7%vOu+8SYAyK($7)i_6QKb;Jq~JzE&U0B&t-V+5d~ z>y%y~WRkSWdSeNznj2muHXgYV5Yt`Y;K&~tF$rPNi4>*ionODc4h|Z!vh$n3;Wcu= z$vn(@=DM;niT7f9TH1wk=N|9xHkCk8CHG&QOi>MMmX?;jz{yz%1hm+q%85dsKCPAH zKO~^q!NfKFmmj78ow@jBP$uN(CsD@(rPJbN0?QZ9C1#JrH_2PV;YO)?%_;H{I=WWd zUpo~Jc&bZeCr!`=sZODAOUP>lE#1yR;MvnZQRP>Y$^wHD^codUR=j;LJAU9_u)p!_BYW4$ytY=?MwH;PLLN|hF;)A zyZYp+L7=#~s%MI@#26X-KyitUFU9Lz`jQ?qal04FqA6>iAnP{?r*CK9qN%?K- zx$()57oIv4Og+10qTLcL<~8xSaESivIc~=}2b{ zSk|%Y0vrEb^8_*I(AW3Z$ltzY12FMnS<~8^`}gmg)Oy$m2?=$QNH$Q^#l5z=tx4b5 z&#kSp@MUw*(qeWb|IoMANJr`9xw*MKaH!lqy*!#C@0sDzNRv}O*jrDz)Ece!v_76mubn?c=us)1XYJpg;=$KnxE>&8oQv)ZcdehfWUCabN z_$AhDA+qcmy8hnh;_fd!TZoQ@eK-i59>Y~PC!fQRCgL`_CIUl9E0U@bc7CSr zs~Zx2jQ+hcpQ-c{yQ>MgaS%FpAix#0C^xHf%B$jRKNzT2sVJde*{Og1^eJ!Y$_ip9 zK?|8Tk%DlYDZi|{`>Xo*L^MjS1EV%BmMoeL|g1o51@+Ai(Ar>my&+Ou-dhRkf$@S0@8L3DOp)o)aqiAJp? zIr|0O_KXg}y|E42{72)=B#LVqKP>KPhA&*r82S2QcMcfE{n%qEW#^mfr0jxmZ<+Rm8l496x{}oz@2_3Jaq%mL%@%QJGcpRX zB~|$PN$l6*;d@}=N}!QOllV@tIsDjABuG+}48Tax6-Q1?| zMI?Xd-G{eTmC^C>yx7KJGl_`=BM%Q-RdzlbTf0y{tz7=`!sQ!k_c*LKCVzFUt%+Jp zTRdB3kmnZ3S(!;1u+8|}KK@n4={x)gY8*F(#|#4CV4NGlo4D|4H{ruOG{rrnM?SXx z9dms_I2ocOf1e5080GWfowHh2mNK>&Y2lhj7Kb4svgIAB%qUa8$pFAxXAoiEy_vYQ zMl8BWe_FY?x`Z zA*L=a4CZPih{+RA$pEjkuP@N~DTte7Tm~*j^T=ZcU?;U3003Alc$d{mUf5-G-g#rY zN%1Y2)!@5h-n`}Hi7l*iY?19XeLB}d7#pk&z4U$6^1)ASNZ+8^)(Qcjv*gB=vwwB^ z4yPB|)%)bPzvark?c4_piA~f+2_zr}#LSul`?V~rJTMpFifX{3q=~=)>XLb)%I*Cy2K$5KX)O5@3VIhK7bIHy^(o8XksL*sC)S zC*}6vbw1NQr-^0Vx%digSAVhZXYa9;7XZ@cH^e(1b+b)Fz%1EE55R8f(Bc&YO`MhG zy^?|Me>?05)O7N9zEt@n0Uj1xsRb zr5`muoNm2YYV(_`oN0gjui=bdiad32JXUYyivq0rY40EE?ZTG%m%~)0NW%q*wn2I5 zL*36f>Q4I`mB}vU7a(b-D^XTx_gc#!Okpan?2VBs%SXi?HMQ2<^LU2^zgOwqNM0L3 zB~7YHe5#ORzIai5ZcWdn#8m-S&;tH{(SoEgXaNCQ5b3dv+lu`%Hr#OVaG6)gS^rg= z^CG!8jFPIGJ_5rcii;f@6L!Cw!Y0s!4C<#8j>5WKEq?P zSu8J<@p$lm5SXt@{)KFGhz~9W!q{4T$Mh-clPNPvjSHyzK1Q=Gww7 ziRmWrsY{qcL#QmGAv?VDYf?zw4YqlLZp+~(n$)yzW% zjfu@+f`@H|mhCNTHC1!8=YgU<4de)z`q;e8f*>E$5=n<-4_)U%;5Mr^Gydt8XZUUO z?&4gc`FBk>;ep}zr}zlg9(^&~j4?~__+*o2hblZ{ur)3;NpSnw9oU1~21~_klo(qY z>c{9vw@S0Rdl=iQX$S8Sfn@_VNxd_*)_@S7NeQ1h#o%6y6KaHB8`bnw8P{>AvOMye_0{1^O+CkBn*$ZZ>@py22f7_Rk^4BzNli#yg9ts%KFCI2Qcz&G(H_xf!Vq_omB~#6+d6 zgMT_GK~HTErgyJx5LF)&JdiP(`<(W1MpX`%DqWsT?7T>kDShPz{Q(`RH<7RnGm+{a z&r7pb%TylJP~q>>g7h1uB3_8mXeC?C6WNeyS~a{XJ|h`@`<5s&5B#??ou-S(aaM-1 zuiaw(^m&}neD7T-qAC)w2Fy+N4O1#QN1_6KYmg}ig}Tm|`S@27^Bzn1dq#DwdHIj1 zcGz!xu5Q87e^>-ID2jP+dnx}ScyjyeD??AM5&8x$DQ|Bp>hj|)@!?GznQ6r;=fehH z7a`ZLd}h67+11~zUh#TyY4p0eW3V+HUlNXRr3gceURCn%taZZO3`PDDhJ>xgj{Jx< zrTY0*VzpUWtixf=8eV?=JBX|he{_mpnW?)fdMSLqxGSq{11$W`Q$HTuzLznqcJS$v zF>RyeMKqnV=YDcURntGBhuAmZg5EB#-Kj0h{uJ=ayzzmxa{$TKHg~1Ca8Q{_?VISb zYUI5QHcMf2+B#`@)71vblrJ^aJ>zDa(?aj|gv|VQOhc{)7sSXEj#+`(K&SV-ANof~ z4s2v@xl*e5<|yB@CNx6}o6E{ZzC@;W^$)AVg%>;I8>r?Fa>VZ5F~jqx6lM+a?*#i< z!Q`tRR#y7THaLeUGujY%u7W2?3**K3>IdX*53Y*+|?Ya|6GDMTkV@SjN z17#C>u8gVUD7lTPiwXYoG%_@7v{p;6A1Roy=0zX{m<$a5I1-{#fZpVd#kxgvx)Cow z1X3u=m}d|q$tBSXrkbpJ>BHm7-w2Xz%V2XdGB;}l7oR!gV7o;tCT(~V7}T%9!!()> zWjfNY7+v4OHxd!@9+{^Oi(OjW%^5*CLf8F$K^wRGr|VRWW{=4dI1e&jlfciLaAD|~ zA13aFMkV}nxk?(Lh1EU0`A!b7Ju|E7&DJUIpFiCk_jiv?SQ0`N8JN!gbuA)Ur`df9=doFf{aAv z_GM@fGdxR8v=H$36Q-Bq@-&r*L*br`0%Pa(zKH15DvN7DBgUwP7nWPA6&}F4#HYiq zs|Xo0SFQc0R+&y=gc`7w#P`XEb!T4>!7jbScFP1U z^rg5NRarML>czl!o;kpjH-FltG%jUfyW`{KHze|cMb}ran_Ftsq};8zKD|LM3%Gq? zEA&xYn;=m2A?(M70(5Ol=74X1^{g^PtG~<8$rSUak+cw*^iEKO_%13gyyO_7d+?K9 zv8r;SXX?Ftl0?KU%T};viL2b7HgvkLpdNG9(5C}2eP>Qe(y~Xt_zC4R0+tp0cC25Y zc?mt#RDCd~5ar6MpILQD*89fiHTrq})SSOm17JxQY-6kJT(~Oe>{q@n<1dLYTq^&w zbu&f-&F}rV>7_6``Fceq^Sdgd655X|I!oqOi~WQ4X;D#|yH)c;Jz>sH^IdJlbEHDr zXNFnVM5Imn`cm32x~W7cI;ueyotI#xzckSjnFWWoF5GJuFupriPt=R!Z4J zlz&Vu+pFIGTT!-h|7??T#}j%--|g*!gBnIP_vr)E*lA<4P+JT^(imC&jA7{SVSSHj z8+uuD)_VIT9?Sdu+SyV|Iq3KnJZv>`;vFzz7t2)LfPsDG!4}MC ztuMMxL()|en3jpKr8Hpe@TV9a!(|gETfTL_a&IIwi!O!QA+1Z==|R^_fRSq0jXAkA zRf-G^&TTBXF`Zj+Th6laDa%LMI!eH`{^0s=$n`DW*x`&>Z*}Vl`t6@o^sBz^K3QNt zlnIi9H*U#lAD*(!KL*tj-xa|0LF3(*cSkKjP2un*sixpWUHsOD4v;ZixxNIPfnU*hIq; z@x(Ce?Q$|rGY3l_un3VZ!S3C)0VB%m*Q~{VHd6v=e#kr)y8x)zeDZ2x`RcdEfryZE zTDtuN)ZvwPg{a(d0=r3)Fh?mVPkU!4R*k6yucX5|KlSBJpppg$B(bvdwTCH=UE6i8 z&xndX)wRQ@Llrys;{yyITSZ}aOUVveP{33Jx_L5%vWF09baNH3R5pw zReRfYmvf7{9U47HN!$9SQ)RiY*e*+8@fvPa=n(m%_8nKt@^B@E6dwn3X_wk9wb6SI z94Z~zmmwRy+5yGZqVjx-zIy3)mWDI$hH~l-q7!zOU)Lxq99+qFrY!WQJ8#TbwOD|a zxltC~G;lz9O5mA(+Bo+lZm4nuw%5P2@mh{)5z*5;m@Q7)$U+7kO)9psqQH|vthgQC47a@+RC!N&;x*SAiUc(Pl*DTvA2vGwL6vZ<8=81b*3eWBBPZ~W*sU^MmXG+` zTnDxzkKOi5=FUB$A#Mn1ejWW4;rIyzI7D;LtjttFj>YbLPZM3oCL4!KMmpbRPlq>+ zWe};0Af)?qpe9zpGavZSOSiqr5`!73?Y&cGM}G%ceaAmjtc_Wk^gK4HGaw4`^8LpB z?{FyX+z}S*_l3>op0qzffGPX^?Pc)5e<7gtJ$qw*UX<))(U@^C=S0T)om`i zg(lFGoVAt{9@tH5efc0e4rYLf=($&J)qPeJbtI(NAKuU)!`>;MtXNE3q8k}^!o|1R z(1aQ*v@&rRjQQ2SgTmktm!JfnxcFoTl0K(zRbG!^f=|K}@07~)Q)XBA|J;>L0W;)d z#>jmK=lIi$yl?Lh>GPZsla?D4JG#9aSnRu=g7AG7bTsj&m=_ml1UB-Y8{xaqUc^IFZ>`H6+{Yl$Lkdoop zod^Z+;v9t{yf}UN0}`17k~C1ZI0Q(th^wlq!oq%|2N%NMzCE2^P~f(;LTZ?eQJPy> zku(OW92FtEaIEWa`#7B$^s``cTlROfbyH;ClPaEcn1Bpldz(NPmm(eEI5=(Gzn6+w zsh@X{P6q!j@bU)ZyP02lj)o0ny@=sg7HeEsa9O`IxW4~umR)&wxr)+aRqc{pT`hyU zu+yisdR;<7S0H3#P*&nF^LsUYy}i1(Zr$?v+SAh$pgU9JJ~2|~{RARCeWx9dr@xW6 zmrlmi8?dsnuJ>ba{QE0oV-{RoTt~xVX8=Fhd(R8Z2V{TkL*3_o{hVc{bpmePq!I3p zwyqzt-+zBia|9O@kRV}Y%FoZ=K*cmNlTN5ZGis4%ia}cF+Qx>-SfwK#Z6y!Lt~tc! zE#|=DA6~rY)$`$&8=*8gbeqh{cIoC{ASK3lx;X^vh8JVzgeri{o$9rKsX)zD+*Sqv zZSkcRx50BCgClOq%0fYYs>${m*?;cqIdu&Ukh)^&1~O>?LGcFr9wgQ*jbl+D)y8jW zMD$feL>G~0hPK2S+1uwNWHJSSrLN+}%Ndc(;GDK7P83L{8LxJY1H^6~XvcMND4+T3 zGL2T|Le^K=&I5?Bq=0}xG`Fmlx3{-q;98!bE11)pm>6a{8kt6uU}tA%=B{czd^oU1 z?vBE~2Xx;s0F5`soHL6}ijCIR)`mI+!b(R+&C%u+pS$#b%fh7jwq(f!=n8b;u^)7> z&j28E9~9jLB!MywQsITjA>O!nd6V19%D9iKH*Tza%LYk4reNM=DixMNnV)x7;W@#a zPt6zJBe}V`_*_kPkio2_qN1_`nB|0og#291*b+e4xJ@-}b4(|!8#$jPkx0-29j%*u)fp2pnzkI_zNUOINg4;JXAj%2H`O4u}**xK6e{Ol0Gf}C_5V{@Xd`5VXy z6jfBT6w%KxX?w%bI~>cWR3HW|xj?M}csp~s+uGZ^03(iM5=Fy>`Y4dLL_~Qq=bQDQ zvQA^*=4l~e;jZp(ZDch#8S5mqD61q%8xxeNMD(Rw_s^X>$ILViTE!J2=u4tkuii(- zr>CPgHvIAn3qi`i6%zPtj*c9G$t$ErlzrP9#8Po#JUEkid4kbgw>6+BQz&Szl)=kf zm5*$W=Z?hkD&znTE9GFSIizu=ZZ?Km`55qIU^<>m;mp;DzHs(z7Bh`8Z0~2}p+koR zA+sH^NhlNw-+zL?{A<#;A0(MD z^GxpCse2eZ2Gk-@MfcUz&=!i!(N0#Eavzxvoaf8W&tJ@qRS`?1`%?+Jq{e_+CuSiE zOV$_Py7xS{Ppct;%370KnR&D?dIn^Z$yfef4KHKq%yWAnvDCrSv#h$hI`|aW$nL2r zTa;N@q98NF+olnoP*_;VWHUWT4U2%qxfsg@ve22y=;O^7SxB*A->4|XWaJ;xEm(2_H_{bCr<&+=N@5jtN@M8&RxvQtg z1Z1w0XJ>N(l_Lt0;+mVAnfmO{^XGeGi_OY1YCK3K_rj04juaW5WM?NX4i`e}eaKfi zlYo2yi8KRQbG7zQo?yMc-PcW)&L1FyUO{e>15Jf#0iOTa%X@^uV33)o@(Fl$tJrh8 z8JQQdyIBm1Uc`(G-X=;|6@b1MLFT-1u~9xVqn()>TTxMASz-T5(s~+b(~U3Z^<8E@ zzF4Evph4BGt-BF1XrOTyM~aPi z<}<@Ul3DJLA6~#v>C>0+ae{m*!RF-dq@=R4vTg6s&`>eE=BBo>FY81GeQ|icBuXh{ zry6X4xRjLCdNs)F-oNPV=yK`GTWAQb8vlMhB8VkL97^PQY~Fw&@*aoVZnTkEmlC)7K6bs6N85J zRng_;H`k{@9#N7YU@Bcf<;o5a1M=!>*|}>PF5kbrMw-lEp95jSFOOCIpS$$inC61A z2m=K@5{%TKgdP=&+GrJ!~e*^|8LL1qc8uQqfGy+2+}Kx z=a&871$yaj`>FprwzcI^YFQ6{I}y*VOz&MWKrVyYjQIEOAJcZ2V$IUF@crx^`!B~G z9UQ&_cODMBIzud0^?(8`PX<8^CnqP2c!t@{^)fpq1k7T}(fCb*G8(ps)tnH^s&;9re*~mgey_;$=>PZEsr!hUqS6ln z5x)sd78aqjR=i5NF77Yf+6-R<8o>onW6>q+l#BcR56XJR^8o4%tED!GlE0NE2{hS& z?Sy){vbPE4!$=ukx_bx|;2Nvofnba*UKVB?M_|WxUC0y48!Mmr5Kq_Kgz}w=LpfA+ z!wSNiNa->gt$2b5(f`}*N}g`!=KI|6bla2)!yx@Hozp~JCJ_{!xnYPZ3>^U67TN|B zH3L3<2zvAr1+kX>>i6F-xpx7dJRmH^hRvKN}9V$on_ zlWH=;UgmGOxa-8E3XyY*OGOI;5erk^s*6PTLb^lQ4x&j@8G4tks(5Uf00C-=( zWCTUUH?@#ryFp9CBI89C5@8CX6N|gpZ0_}KML<_rVLh4CpS+7%;{FsO@|{UTTx-C; z&t-(6M&WnHd7bWe?YgX-N|%A}Os_2}*o2EPFk70{CbQg*ShrnQfbpW%`8axGX@qLz zpT^Skj+onsEE|8^-V7h&2YPxguwN!ReZSVUX;2q>xCv87_&(C;&!NlkE>k#S>oTf| zqSFN;g&=B;BZSrfqEW|uQfDpkbnn4>`Ne4J4Ux#E=B}Z8=O?A^THvVY*=G+ za7nrj5)|82z9up8b|~*nv+yaK{D4U8fK6>uQaWVkXY`8V#rH)g+54c_NQ!7tb)<>_ ziBQ5|p5+;e5A>cF^;;EUdA4Pv{aUZz=B_3-*j`?lSep%&DkVwb&&1zsSX;sQuAV-8 z3hA3-u&`|{@p^7)nzaOD_XDD)^r>xg07{I>S2@mea}%&N1qapoO80XU0J;diXNeQn z312x~VXK*(_r_5<%8zema{5e3-@9!^-iu{G;F(NLbB1Jz&%Jjxn_IWi|ItDUGU};s z_nNFt2pv#ewLK`<&4bh`$gS6lmM!x4Lqsqp4%Q`>gcAnz%L2?*HM(8N5%&S})6RV` zoX_op+P_4(4?2CrX-R)q21fovwv1L*LqCkKQs=-a=hfS0)I(M!#)|=Pd{~JvaxEQJ zT%8o?HvHl`WT;PSRbF*-i?1*NcFAqx^1~I$GjH_!26k}g zka^W_pE9}WkV?N=>~2TTJ3pDL_t;R0x7h!{y#q*irH_vg#5mKfOzaO^H~I*>;`z9% zo&h3lQhe~iowqM*pFD&Yyliy>R8pUih6kW~zOI03=7AO*ZGVTV5ULbNquZskM$`O&0*94%t>(VgIUs2KGMd$(B<2?`E zB1L&jk)*B|pq2KOx;NY2;1K0%Z2qp+_s+nDu^67LumY7V0{swr+`?086f+D+=bb=1 zWj|+KeDMP63|n|L;ES=Imr7k#X>o#}JEQgSeXvf&hWH_%8EqUL=Pj#oH&fXkEPgEZ zMT9-j27@Sul3045)r9jd)2bN{t84fbQTiI##LU7dpxZaL7iuhjxjCaYPk|{uPwj@X zv8B_gf8;wlQC|m9sbPnCx`%rW&s7xfI8q@D&ps!}P@y&)1=@Ti75w1wEl~ueo4zJR zE&y!~j{P#hwYVhhPbLS%#s8W+?XJY@ZhO#grMJ|^#Y1n4qnTjhQ}rGQFLjDitDOSs zpi1I^o0G`gyT_!0zkJf=n4w}8{+3}>tXHQ*+vd%_50noJ3|%_b8wf`6 ze|hLJRM*uHlhe@e|?7K5m+qtKTRtVixX=QqF%CQSQFq zAJ=`G+IK^%*i=K#-Fgjj3T|XoCuJ-Q>qxPCJfRt3lpd+&@D^8emA=fD%>RpWG!VFyb>+Ib{))jM5Z)9g-dxX@8i z|G?(jv--0OE+T0DEB?jFTU!Z#w`(VZ)MWsbmXcWI^C$pwDDhhyl5d(C85y}sD?&1{ zbU-3Z{opPG=udewW77&UWUQ~8uM)B|S?-?n_kMJ2Y*MGg&=f96BD5u8@@uG`zCQB} zfbGA*@evfySeHI-jo4YP>X+Q!rJv>DL7n6fa{<0Pk&d-YD+|vd08C2@+=g-C&MLq& zz!f6I7l&CfE)zoEe)%ZWF5S{Z9y`NZ)WILrGw#SzTmALJ7bMpqq0T--!wSf@@bu{p zintqveDMQ8)6EQ;vip1vo;+UyK(5NFs$oEBl#(!i=7Y!q;38ve(<}LU7Iuqk=Y@6N zfiVK8!tPW zUNg^`I3T#>p%Vl$3F2l1=3;<)aG3n?l2gxzY{0|V)Y4-CbPR?71;w%w09jnB=-?OM zkgW=3Aeh&F00<0RJF_NB43_ZEFst`*#O?1;m!?}<0KgTsSNDB+zP;0zS7NCl8M^hO&=Oe;we4In|w z#R)Wl|yJfU(s^EH?WH;ChhB z+a({ezqietCe-Z^%UrUw;~Qq_GQdLBnI$Bk;CKWJGXt>NL*!KOx-KAyi~tIn4kS|; z0E={hMw-{SpF0v_y}yft3M~$21h%N+?vMK|4T&=o`kA2O6}1Q#aKQ&CSF8AN049S1 zcr3Ush?pKQRa{hb9TKRfotBn~AOh3P;vw?jZ=0A9g9hWWbLJQg*g0 zD=SL_+2Jt)s-a^kQt)~?>$`%>;Gir`;Tpk)NdO{%H1)p$dUYP(eV4J5>m>h6T29pnK&Rq8#0GdYTN&D&bHGq32bUGLX2k!|H z8xeFjQs^!XEUdh$6kyAeLi$cFMQJNbC8dVd6BnYT!CEkpOXe&97RwYvX34l^l~c5s z8Rlt)oq!Tt2k>=qK>C@N#Q>Nxb)I8$M|l-s&!*r`2|$(>fYV|FMA7U8eGgDbYnz({ zAnR+r;O3-wu+w5-9Rq`cP@u?{3Ge1LRhfXwiIIQKU1L%~38Katal*RNs+(GNzVkMB zWHx;IEgT&3#%n#L0RABRbkv}Qv6%{(3P22=vffIv3oQm9LWeMSmo|jf#qD^J)XE5hxLpu&8_hit0_liSwIquXP11 zr26>gD}N_P$3-x7pfSij{WJiJ_sS~Vx#I%pw-QTi36Mi3-L{V4<>i%e_^ zLToP8OIKW^mbKJ;fP*6h3Yo{ht91deJep6*l!?KEn=`oOo)&?lIDGdY**RnITI}5% zF!-+NY1L^sm=BOtlZCUhKyHcv)$(rwRW|sS*Shz-4xs*e z0-*y%hPi%3n}A}Vz?eb=W`(tHI|&KwC_HgqxX5+5fH_x{?YZ;rQ%&{e5#-eHncV37 z0nYrLeWT}v)z8WuoRO5(comY@VuFLSGYp(wrWIx$X29ApK!Ge}8{GcWu-d%K)DD0i zT#A;T8UXl{5CqVaOspL1w`jD!2B^RPqrLAA#Jd0A^;DjELOs4^l%$?$+9NV6MW~dL zy;9a~Bzw1Lpn+0Zi4Z~=H(8aD?5)hoCiAx4IM=)H@BDH8IOqIz&i9|+KTl5?_h-D{ zuW?=1>v|K~bzyca_2$}*R^#74k~?JM;YrPL=pV}Ik8A)R&8GrGpoQhdv8DnqkCE0) zwZfIMhSZAm*5Wgh9Kgyr9ae_=y9WEapK09Ah8 z)^?PSQt-96^$I3F^_|8#4akxKX}fvr){Lu z`W)WOYZNmrCX)Q^Q-clDA69SNyJN>;>=Ks(u>!5O&m1z|^lbN;U$5(8ln=t&Opj~De(J_7P1TiXk9jy-zVYz9|gF*p8$aL@?tnQk;Qnohl%oH@sA zNpECmMO9%tWlBj3yAN}wkuD|~5xs}kVH$o3Mo{lZvf3&eF)5`Dbb%=D`pi@>$HGmK@y3Egg$+*0t@h;0$O)Vn4Q?iKzj! z8CGy?e3ZAYhGD`S-j!30-0T2`wRCfx-;_evvLRe+^7(>B6| zHF^^3>E+Rhoz{e5tfN%6?b}lj6)eD}OMA|xZNT&+e^D(rH!a2dsl3;FZ$uZc`y|*c zbgeAAl(a?SUk_S;ec`h_ul(0vf04PYsqZ&8?gy)*(YcZXvY*lrcZx7%gnag&&amq; z^jTS=VyYxH$3QC)f(7D>aXMVeBr*#m6{a=nepU_!Y!MSTuimzVU~tdhzX85j2{6rh zu5cJrH1$G-^p|*peI843ph!DC#xz%!m!`qu^(1{1W}Y7H(8EqX@93C8<}0!5&qaAJ zSUy^qJqq)@dw1{Fp=NTN8#nR2bxZ#$j0>`6F<^E!M(H<6h@T<}oW>>0AlYfxb~?#4 z#+cSsecu@%Rg5#sGFRr$p-ZV|nRJ31!OyJ<0*IcUlU?7Wg1_+k`1p_=Vq#))@7_I) z?$W?2+7g|9{*@}51N(b+=b#?rJ%N_{=5tgi+e4clgp_Bit=v*mDhj^UxN6EO&$KGneXldzn<61 z54Ph0X}}#wGe(I8L~Tc&C)RW&hi!ABrW(w~q*301|Esjq*nc58q5Say%4mBI(pUsO zT*75kAJa*v;mZd4w+`jv<}F)ZW2n_ItIyvck%gl*&E)3&ZHF!{^QNPO-m2i8N1V(m zD^F(PrE(qY;TwaDBmo|J&}kfuTGA=>&If~ee%qqKqhrd-6m09Q;ua5ie-8m7QCyzr z>$h5*yNL2Blf>sgR{vQBni?P23`)AnE{V|aB7>Fn7~U9qEe}N97xo#wQ^9}u))pfbJ+9e ziA2vs*oYG+?zCo7%UM@Jzh%P^g3BcFSq*EjF>cJ~ux43seS#w@#oRY(@VvLS^% zZYv4Jec#4z!InY}HH65OjjdO75tJt7|qw zrc-}}%X2&N?`R)FLN;?Nc+=lW82!?)$*x7tgZ79I>w@MSCP9hAt|S2gznlF?8d4_; z*PMBNZ7VCQ>*jx_gu-ky0jvByG&y`Kq*5b`Eb57&8V^UKB-cUfwCwpJ)HW7|Gq@wD z3xeF-jEsy>B*vW>k#hd^)T$+GoBZGZ`q#yYzufTZ?6eCF_ww-_$1a=q#*KP54Le@B zLLA0o@OTFA%Z%10r50Fui@Ch+PkvF5-A=ojGq3R@wT`Qo{`b!t)qWxl?b5UP?p`+{R?hdxo^PPtPg2}ULv-r{P zKeP1nMqk=`ls(*uCg?1Bgzc{1{}9klJM3qoo!tNFM-a!TWKiWFd!*k#waxeP-R?fH zlXhheN-GM|PT$5&)fMIC^|-x6qA@^9?i=1EWUYMU^OqJwk5d()LM^D3Ek*QKE`~|k zU4S;CYNf)wCep@mws&C`_64WcZ>W|rJrlCDtkG65=i~1kTY4 z!*1@ajKgDItOPzE;R=spkJXf!tp3xiwI2MW^iSO_vHJD(tyN>fBSH{DOGl~@q!y~% zd9@?VMcF+QSqYWht}oBUIvc4*i)f_m?3jJ*D7rj(U>)iWtY275ntdrMQbdn5)c87> zNJ`MXtCTMmPyf6qL{}b$&?a~Ey&7>8LJLMOjX(sCNAt_R@Ymq9F!8g5+XuaUVSav}h)A7h>8$QT_KRMR>7mDV-)K<} z^#>g^HgiOt&1+;Dbtfti;0u#Wo1Ob+HSF#Siknq(M##EpKrfW-Jl6TD($v=O*-8d&7m6pY-@ej!xgX-drC(rd3JVV275v5VcTPy&SUu*`uP_(9p66e>A=3u7hDUH; z2Hz=tpAKuaw+U*cgcHyK3R%)Vxc~6wXhh1RO^+Yfc_3l57Uir46tH*%ag0pt3BWek zYobG3%v*?n26X}nQX6>W!|mi4x%a4tOGP6;>ralxU{#(Q>&z#~!}s=W0&l?PLg?y> zVdDLY5fZGY=ekyXZ>47~75<1G;aW48>hlRu^3zty%0=T;#Uf*WlV}Ay`PYpBc^f^( zIOikSldEOC2@jOG#`RgKKgbLrJ({iOI}}YA;XAvmnZ}e;uQIq3gAxuPGL_PQ?%hm*_8-phO9(TI+M-UNgY@VbB zbh^VS{jH8Dhu6^0S>!DnOAw`t~g?rG3U)>Ddnu}|WV&z{-Qcl1WE zO0TS0%g6`;pEd%`1>f~-L|BG(Xn|*}<>~ePapaHpuJZ`HMD#|&sjDs8CJ!Ru^;~Lf znz29Pg&*ARZ;2V}K6RVbt5*wb=R&bnTwMGQb>yeN)yD0zuBVE8QZjvN(D9!`?Lk{| zb?fEMMjgm6|EN&dn5|Tp`S|hUMCi6v^wJ%2mqe%3SNRMpG^Niir=3pG+_tn0jRD#) z3iM0!N6$l!JJgbDXCG*1f;bCU!>YiVGqdZ;N{X1sxm4I%>30i+m zjU@m1@Igp#8L*k z{=HsHR`ht;=Dq&BHf#zSYp8awHaNBQug|EqJvc_ej34M6ey~Ln3A(yTE)gO^Ji&8a zw^d`_ASQ`H0Am7n1Lf{*$=8TI-SZ1=!dsK zfFVf4p?f<;1kYGat5&au{CqcU68VTom!BWBaj#mk)}p->-tvsMDz7;6Hiw`cryr?7 zfWGzQdbDj)myO1b8y+iMxq#M-fDDqIu8L6I0RcKy;KL5bz*LAaV-a?B!c~%xkclfs zaq%&1R|=#nsBJx??OyP2H8+p{cysMAp!62}=`=NPR&M62X`kff2XZq{iJ2BU{rv|H zs1aBTLR?rGX~#Ynx}eP^=m6+0LS%QODMBkEDbxUUGO@_UD?F%RhDd?N za)ix*X8;$<<3oRYq5|^x2YnQa>M-?kzjkuY;=^@?-fzu%8g1hFRS)!N!S5aqR=5a5DWuNV@mqI<3NHa{p z1N7O6u!uSpA*~6NUD(LQ%G{h-kdZPLOQ0T_-RE{(s9Q*NHI4Rk9~M8*OMu%F9{b3=%1Sq&%MS!u2nsGvJsGy;Jl^)02p-Kt*2IAbtAGh z5%dZx4q8t_GF-iKMfi4H1#a#EkR&GhwX_{A-E*R@1MckSJD`?KW7!0_YU>ESRSER(5~;r*#3I|Q*gtJmVc9EX>O76 zQ)r!2|5~-j)AmhTrZSdu%1KR*Q?o;IKg|877u<3NJoz2%GeR+o_cQ3;^2h0Ese@zR z(v`gyrid_}^g(iS-*1v)<Q>>rT=g-#c=P#h2eMVh&;uf0ly+Bt< z%ZjKcSu=*#1iG`tyPNi?5@1jhI50Q-ddnnpA5M0L0DfsfG->54tbGSs1n7JEy5m4y z2t3DR^BD^-5%A&Qt5C!DaqNR39x9ZIZd^0pW40MCJ*3?wDF%T`7fF$A+T4C->@g?$e&o|wba)3)bQ7S8 zB%kEu)vW zPZKc2F^s!tQ4`7!8LOn22u?4Mw?+0Ni9)^{hG+0JGf-ZydZ395K8MgkRLpvw#{)38 zL7)O)eHw)J!c+zxl9hcxHbFYE65 zf29_!c&v?Tmr6h*ML*_!SzB#0)D@P_?@|glgM#DnvuCd#KKz@IG{?6;PY0-X!lcq(^g+J zL_igH95{ntc$CnpcmI7h3u0E>I{A^OHBIQ<_f`xpUtaby@65afJp-lSDqLAoM*tD3 z@6h~%A^WVMA_pqLcqHDk=)A>GDXuT{hypeK#wfZSm*d6D78m zx%W#GTEC~hF;KdD^ta6QisF3eOwiTz6tCkgieTC2-E?#B4d4GQhyCv7o~N~G->%m^ zz8~B9_w7!IT4j=r~kCHy5H@+o$-y{TNsRch6uf;e7Y*U0gw4;#wX|+H8~^ zzlJ!u2XhIyS{q){v`Mta>#{R5aK`# z;-5N*vPLA=H{PWgSaPJLc#N%sK|<2?NrSCH1-C!=%lYIs0;fKXjPy2W69c|e^7%6b zj;%qlsQIl>y3HV9Z+)bQZA%rOxwyl-+uGb|Cmb0BRapCv5!67{Glq}WU|Q6#$aphk>(!kJuWN)b3Qu(roQ zj*)jQKV18x&&~JE9uRF91$#)y{0jJ2VrgO?F@%>G0sb4bOt(#yb)tWKD2&2O1wXD}*;kD90c|3S*^{${Y-UZuXUE z;HVNPg$cWjkUaD)Jw7M3aAD{GzAK+TSwT1i!mQvi5UA*71X3LNruL4G-`KSLTu-q2 ze%O5)c}|pQ=3Tu0Gt>CvDx{~_T2o#`qaS3Hw%aO1$f)z-CEh)IINn#HU0_iu1*=}! z(3)jKIU!nKUmt+Q!5I{A?HWTju3iF9=+d!c$2fzSvFE#?mXNrRtZf2{gbP5u2v)SM zbz4@IE5IFQtUh$PO~TAY2{_E<$IqV^Whcval>*5R#N*u>1nHMEC!&OCczAdKN)uC% zPD&^_j|ak_RFZcKR5Ox@TLE|OtTV;xQ$lhjJv%2d8H8)(GMQCMamj&zlXvaiTQ)vD zEm45yV+vZxFEljtES%qV!JhPls;Ve`X(!tIccN3De2brv{P z3~Wo4w6(PhmSv3ez(NVcxeV5qJiPP^(w`6V`uXmeCZf**pdi`;H@OF(e)q(jd?u=- ztNUC8C4gd(h}VK+kubCA%JPyQuPI0#_RKAQ_FtxD$3Lw01NZPKIM|eqP~jIG9QLs>BJ9o}7AF)*_fW zWd`cGYv<07>o#tbL>dTev(2sgnq?DK2b#MGNk$4xS&j{OCv*sMzh59ZXAAfa3#RV6 z0fCA}dd=wI!@zLilDi>+5$D|k*^8L>^1OKr?h$tYkd=M0nA@e@q%ushMhdvL2s#JF zpb11O&Mxo8H#@%!;L(_H`pxI8XJ!@$oN2+YpIs~b?4%M=R`qEmocZVq(4TN5jSnxi zgKFuMU$=g}DEeAQ%~YKiQYxanTkr--QKnuRxtdbmw7jyK;roRvO-E{rii*a^$FqAx zkF(s^rF>r;z^Yjxf?J;@U@lxN{A;uD(T|=#xBB|++ZJReQRZD^ECtx7P~Nc=2?uPH zpZoCe(gr>}k5Z_`42=W(i<(OmuoFuGX;xJjaIWDH)VDYxN?39l9Th28PypE0FLR^0 zV^~?mcQ=t=gE{@*t7c((xN6$n-F+)6r-Kl21R#|eyvstDx0NhcaJpvv+G!uIT)ira z&dA~t#*V+!k^di?luc5W^gWD+7mKH&5bM4-f5o!yBHYl`cL{}9FWAQV>Vr2y9S ztlLhhs_u8pq7HMc0LXn5Os5pnv_V!)Q%gd{5NeoZzS4~*~uNw(ahxhlnR5-Hc4JnA>bg=c% z)!34E0C*npYGSd<3P?$Pl@)@PBB*C$(Kts+7Z?fYtApU@MSM_MzP9V-U zV7XuB^aFFopXHOYz(@MzAYNEI%+Jkf|N6`!Xhg~95LRVhi!P6?ZAab^yYlZ z%E~J5vc>d{xa4L~sf9fmwCls@gNZhh9Ls<-b_~juy;=7W$~7?v+k~?n(7LLj%S_?5 zMBO$2L;&?cY#yb#T4u!q*foancc@_eIyA&Zs}c;~0KhL{p)s@_wY6uk^i)t%CZiTy z|K<%E2m+cAq#hbc103~W)eu*8d69zOi<}fncoIY!3&1i#vCB(~2jGqYu%1X|i7Esq zEUlzmc|8sOToEb*@(MDrzfeJ)CUe6WVM8C!kN8S5?AORBrij7~XBUJ*Leap(WaAM# zzxInzLw6Mn!MOtpp~=mB^8PQ0ODa*FfpCF2@E59jxQt6RU1<*C2QSP!0-fhpVdHBW z8XAtMwxRaL0TpjfOh;{u54tzCQ`NqUcir9-XODjOg`UB#z;8x3HZG2R_$rQ!EKJ&P zV`Z^xg{%ysjUYHIV6=E>tJQH>(n~N0M8HPK7`)sD)cSxs_Y>y^c5}mfgt-ku-$l>~ zGKG!^LTHw0Y;^X)35*^S0~Fv)7Y0T??fT~*QqxvF6;UJr3eZkI6>5jGUcqIg_r&kt zjlGaGbTcd-ks2$tU_n*Fs`1S24cw$WoS&b!1Pw>n4g%Vd!VMX;&q+#JdOtV{pvnd0 z=!EhEshf3uP?R>IswXG#k!tex?azE|ZG}EQD)sF-4s0MH2;c!!vT(j4cr15JJ|{c- zDfN&=?TaK`bzI6+(MAQhARoc1)k5GRCIUnZityUEah0I0KW|BK@zZ!LF@3C%e77#C z6&Dk`NnochRDyRCT($NFBU2Ah5mZ4>?XosO9|ofQsoh>EgXC#CtxOR{^61!5tTaG% zA+Tr99&QD1eITS{F%x?YY@7ka=z9Dr*Du~RWo5!E;ZpWT2@ZuK2qqZj_KHuD+Acsp zTTgHj=$T0Hfc8@BZ1@OL zDZ+Xn3O1S(1>lb`Fu$sb$_SYjl6$-8ufQ$&@$TBU?~CWDMx^FThRbv(V~)-pq<8^< zuLSJ)cY(2E#|KW}>F7U^q$dutY?F2@8-RxS2MkC;1oWB*I&!J@SjIGWs!f|&hHXdd_|%m6 zjT<)tpljNSP{!Eou^hL%-_F~?-iEQmu{ZC=0z5!W0Rtmtn&)NHbj_mrKaGAGB z)n0LO!V*j{!MuAt>QgB&9d&3liqJ+_;B1Ar2u3mJ1N@--oItMVr|4a{@MHp-IZohP zO5k$?pvB)x3MNv!Li)&wETlxW?O=w8A%>JGlntcpUN!p{QYaSB7H~aEFmVV#qT+Js zs}w;46NrfPEGh|yFjZUBz4ZMtG-Q=w)l^OZ&gh!U;PP1^LY|P6^#2(6&a7&yDeMCn zm-XrH!28o4MMfI(x3RFW2#}PiKi8VORUg1f+T}P{laveq0?wrEwLXTn1`pTqGq+0* z*FCpGc$MTp$NbsOBwT;zLw8hJFrXkUKJjk>ELo2Q5gQM`gL^I0h;WHME6Z9-a|CtI zYO`%1h#!J%`@@;lzK0ZKzd`W?@Ob}|ozar52p=UY3NRmfKNKDPp=o?#q7JQYhVzIn z(q%<<1yH+Xa{6Deo_QKn#@B7l~x z`#bJ%*$!tpz3Ndj|EodlDB6XxY zw=j?lMzmAl^eb9lVP+}mL+u%cYo{*4L;1s#Lr~6G!El9mAi#_TOS+ZsHy8pdiRP3) z@Nf55d`K;_<8CzEh{B32I2o=9besKlCNef9bqc=yzyJL2=kVX_;Q!-3*p~eEY_R2x z%r+K=y(Y{!GzW)1T&w#2KP~K%!dZLH8Ii4-w*B9eWB;dN`BnJ7|NPhO)13eK5C{XS z1InLU1mdb9y;8S(XGd!eTQyldIcP0qv+Obc{mrCL8Ec{U{%A$m!-U-1_wCUu-;4g6it#j}P>x zVD4wQ(?b!HX@M14WI~j>{l*UK$lzcyMRjX5e^!rgx7pM9_&77mj`!zusiPqmlBJTz z3Wg7OEYe@^6BC1aJWVyq^PP^L$sr+8QJqJRzL4*dr(cO-=O2h^ese&-Wv2LZJf-Ym zuWsJl;Q}{%d&_(NY7N&{i(nWk-nLNgFs;8UeyapMxnUj`khb2>?^7Q>6$cIbvhN(f z=+D7ZYaV5@?@DLP1Hp!#FHd%+dAbQnP}up6jI?MOX=%JpBMX7fBlq%bZSMN3ox{-q zOw89AzZJ+GtqQAYTwbbcd9xe8I7D{Mk*|5Pooz21`s;D9B1?v+n~&FumjbHVeO*_B z_t=$7+Wh$B1=>ZfuaJA&uc;BL7fjC@TC?r%ynGqi;4hRVTBClt8gncPhSV(Cg#l4+ zSx!@tk=OCh_lY~=nCs8`Wa&Ejw9|VINzrHPAK)%mZ}dzJ-`1@vxt0vyZa(t&!Lfll z2X|W4hUYTWc$DPPi=#=e9v@f>m7B4D?|q&JEq6Cfus7^Jt#3LpTlOf>X=TZ&(nm(> zD-KANf!Et)pQT>bt--NEbq)`iEbG4--d(=U9~a2ororRoNKL#Tz~|u+ylRzfWrJXb zd5w;Mk@tY=!QUUwi4PYNu02;H-}dR^^Yrw5r9i82yKknSt99t!6A#Pfy{tI013gAx zTTaXdtx#Qj-fBK^TlLw;_iC$d^kK$`9X=PCG+SgFy4Q&6|EiR3{ z^an1Dx~#a>wX9+IKn)Hnd6-w?pBnh?T^(b07rwH-^ga(y+ zjZt$v{L_ww&&|B^8||cd=b2vXKmYu*+cRQ#!>j(`q~X^IQnM>=`}ddja7RfyN&mTa zW%gw%6T>w-9+?|gBCBV5$K~WIVp3A1y1q!->g#MfG^sN>m+B+uR6TFx1#m=M^jTer zj+h<;!#Z7+V@J;a!SJEXT>0<3TFCH%?9({@^CtQ1bT5kEy(FYL|RCRnaxBMxN_lgv?^~{2~KGfc{bZ!l^;c zvqnCbJHOg_(`IxO%xAyfTwxb8&nmsaC{!XhORwOYPxOnT$I6e*HNI|`m}-bHk627H z8mY`(ZFQ_z-N(H_*ysoY!@c9{J~3+NyxTeE+EuXY?WHe;N=98AF5hMCFYfi8uZWoV z>7Q$guhca)arw}@eU5uAX>xc8IP`h$@t!N9&`O(==O1Wi4s|)$>BzZW*>NGYlRZhx zt|Dx9X{<#_v8d6`Q;>mS`E8C4v;H-n6<6ns%<1AWpRNcl3T60iR#qgZLV@hUqI#cK z@3CXW!RnpfUu^f%hK;(oJ34quJM($^OLcXfkEvDM=afi{H5OoH&CJ{cOuM9{AcTQ| zc^6*Gb??PCkDnjc_HPd0k#qk?D6hO+>2qu5%C@8nOAUN_6~%YiCi=n@{IL!NTz^@! z&fVl`EYY)D;@^GheewydjSSZ=rtMnCu{U@N_i#%WTj5+uyX!rC)sw#V7r<_Zv|9el(){XQUa7wKJ*I+l4+1vE_P-NIL)8 zMN-ax?7!!91A?D~$m;xRVfjFw_NEha}stDDoQ>$hvp}>sjjP!fTsTK8Ww|#M~(Q`;%|U`7&3&toxV> zHRPi&m+s}`1e9xPW?vPrfH8o`llA7!n>lYGbvO=9AAs9Ek~0|kIHswo$;`r10*vSah*%a_k; zX>mX$bM@o4op729a4gz>E~2x13v|W8((-O|bF((&)wbTa2K0W#xK2}$IA?Wqp1E`aN-cqQio2<)39Ws~ z*E}}|S&%sP812n-I||O8bI?z24aLgpBFN7=&I_nCCrkt^5EI|0N+IVUMBZ62*;jn<14g8{vNv5>k9Q)PTK4mUgl-NECQ6^mxY$~^XNzD^GWdU9z`e;WA4^M12Zo03-4SwLZC5bgS7w%; z^LIPot*Pnh*thlwmx=@nVGq#G%+!;U?v<|U#y3-!e9pD6jxI7@v=!Od=#v5^KnVFd-+5D*7 zie6gi83hIgPGAtR1a6HSSQxAm$Jg0@d&f63*2RM_;TZs^(tht~DLdVGZel1YwG?vA zi%@U9g*&J=sKt+`ObV(ETsqjWx&pzFe+G*EGCf`Fd01FC(8|g%Dv&B|NMPRzA{Kq% z)5`d}(kvZunv01;urg3WT>KaYDjQo`KC@}*KjN~pu?Ym!q<|lfiHqBfIj_5}&@y*p zig2a0KPe^UJ_bAaFamxP!%uer-;{!SewmeZ@N|@XL>Lv@(-p9?zmpE`PcSD4YFF<9 zz)BZWS2Or&!ohZKaqZ#6b~Z);!y;tK|G)X~D^}K#k&zNZ6o?E(Ju3`SCC79mY<&#z zS3bw%+K!h@94||oUcHPz7(|6d#RP;U1w;>?7ZH;d6_yr}fGAH|Sa|!%m(Tx~AF#2z yWNGI1zyAR{)~%!<-|+hrEFB!}t~y%U*)oVoh+bUHn1Zh`s2o3iEaj;2jsF3M!5d8g literal 35068 zcmeFZ2T)V(yDy5}PX+W-L_oko5fD&{2ndLZNS6{yAfO;MK#(e-seFP;lhCC179c?& z1PBQ#BE6G@j?xKDNY@y?d;y z&U%9T1PcobtHz@T1}rRxUbCg6m8i`NYn7TOaQ7KM*2ESzpBHTrkJiKEt9>JM1<%(X) zI{sGE@brSU-3Qkd2KE3Z_|Ta&q#TxVVmeXBwp1eQI)P$#oG0E@$aK9{T(D3+A=; z_4R|e{&RkQeje$Kjg4my9{{I^Z+M%TnVr(9xB{MFYinypIK}+ct#1-?awgv1-V{u| z-N3-Wa~K#zmX|#549}idYhEKvRJ-zTw%B=e4{1^3u?`wEy95wO>Ey{6@|C^`eFA^@Hsaw+vO3ORR$4inxs?r}sTBK`tS; zBG43xUQu0MJ*xD5w%Ijb9LMQr)mYOTV)qDmVcJNuYzfTnos3ggpcl>f;pPTz=VHar zC=Z77xP~~C+%+h8uqqNGe_Rt48X6jZ-7L$v1X}1kP}X3XQ&+c-!%kXhB}hOz)08+a zT(|}nAGy#lzkeQ=H*Dd!b21bCcmRSW8tRa2FLInsE!^s@mwl>TpqvUb_xA4Dv{FZ# z)Vbpqjyc0c3j?;-${gB%FN7~G=|5y=WsA+u)(G9molTbislb<%*DVRD@P4-y+ef-C z!K<)M+prr&_wLK-w!~~s%NLoSZQVUQa8JG*bm=cuZEkKp zar}5!hH9`8-W9gk|LHz|;O|Vkjua72PEHL2C^YFPJ6|!UmA^j0d(ptzxj4;lN#54p z9!@DXgW_FN@7%f5R~Hl{4y!RD`fVB)8JC;`&cdwR&Jh?-OG}GEdy=fI=j`LHtu4o$ z-zy59vmcZhvsY!|6zwRbjgNc*>-qt|z8ph8&U|fcZ7t(5t<9_8`6x0n^2A?%b^rJh zJT*0y>oQd7*pVV%v%j~Eb0UkTD*EWgiJ4GlyR)ZgG^fRx?(5UedBCn@rXxf}t!`o^ zm1-?yk=kl5dt#P}T-C@KT(&}$q#A#?3TE0R;9Q{4eL7;zX#dNrX}Z6nq`{D!u)~5M z`8tK4Bf5|)J(cqFkVs4W&AwV45!OH{(+&y2U!Fa6m9E-d#hsnAuT&2(e9bDxt;>ya zYm(Cfb5Rx}3+H&_3JcRyD47AxQl|>-c-fU!oR6#>o98D*Q6$B(I6Q6{+U};x{%(FE zTQy>cTHpB<>_jHT?)?_jr6)=9J+*g--G3c6#GZw)-3h16=vqt*FU zyyjDVSR6KDZ{!Nti^+?qhqC85P<-g;f+WnPm6b|}1XOIC?X{dpLWqjt7k_VsaqW;i z9fgwMZPe7_L_r#Rtl^Ab>nwdHa&o?PWGJTMiE))~`lEy*0&!_>BHwRM2?gt_++5P6 zq)2vNTQI7zuvj41MnnN$;Ov@@tF9huZC6^>{zwe4)Uj<*NW8E85Q)hPaEA`v61pZL zVlL}c4ga>r0gX_R&{BRC$BO4MEzpLYnf zs7#4Ddg`w}b;Lyz@5-qG0&7r8?{MMMBvJv!cW`?mvK3#P4IY181B$!yFl_q4H8C+0 zA8*O*^$Lrr=?lp5>Jv$_TjAE(7*RyUe*944?Gs0NdD(Zn z7PY4!-`PBX>3T`31U(UTS@>f1-u;8N_6a^+^m(qI(0#uY&_0gS3t8&whYKyUYxh}Ko)0kA zN6so#NUGVk$QSz5xl>&CWSbv#lEH#GF~XUe0J@>>x7-Fu;5|A$oF{vKg(XH74HSJAlKuH68b@Orm8zlfru zIeng*5EB#Qy*OxV8Q2)f3;c#55lO#JSzGH=fe%%>Ah#Fs^u6t6_{8^!_wU|CH^<`7 zrc2vLf@px=ESx^uDp?m>UJmY>B&dPhrBgu!k!PPCV4VFhqRbTg^a65o#Aj-5uCO^q z7(V{(or2F&3hDRwNextRaPau}___1v^L$2q)nkNo3`|Ww0+(a6IMa1c#7f=w#UOlY zyd3PxH}y6prZNaM3DIFiso#T4VXjNw$US-sk~XF36oki8_5(Y{)A#e}D^5d&6%92W z9=UicCDDBrdmVFAlb9K*zO#0GUJ$elEBL%rPDJ9Cmc4H7JW)iJzBN3k;2{MIDt;d) z(4NXIgy27>vtOB(7htFdb_Q9$7A_56{)cr|rwi>Zi;A&9&IPaP57IY^)H%4)qVcZB zVgk2CjPI)-W3Ka6x{Jmq8?CHU_iJYMcIq?Wecn_pgmHB8b`T*L(wa@54C9csd9D<< z>ectv>?bOx)Nl^Wbqx3I>yOt=WV#A&a>#DBN}o{r=%bSvoUNf)DS+N{Nn^Y~T3$#5 zs~Rj7`oORP(-u--3+>p%VT-xceO^O1HBKFvsfixZLSrrejU%-($3K7zxH+FjK0~Ty zU{YP#E6bp|q(BR#F+A{{nCB2b>A8iEmsmE|_*UPxRmkkGFXwg{HsP-4JxIrU3(qgiupNE7tvUu-nx9ZoKe%BS}1#Ibt% zM3pgqw@dPqS~D$y$hgI;H5a_n*j^u<54>Z_#rwVI$>jcb z(J}F68Ie zI{&cJbp(COWw7Ahn-i<+>wWd?D#h#T`x1}&ZaIHU3)=JbMIu|;+jUByxSsB8b;+TS zuCB+a$^pHbt+XkI?s4f9E(+~ zk~2GF-y`d0`T?xG$*IMRV@HoT>@Lq}?%-|@n1vw0l6^oKXwW6YtLNGZYpFV#?vWN2 zi<9Hl!g4-}N_);nh&Ln<9|vjtHlFr)DYU@|#M|3oVtN#~QA4;%Vfp8AE4ZL8K{ep-2w#)&zH2Zq2U-EbT2TH_A;xd2=>4A?AMk zcp;TAq@kP3vkfY1WW`|Gw$5Ze#_0`NhcgU3n1Mx8lRI zYsO66m(^|rK|e0ldE_^A3M0~QbV|}lv-eVK;-bUCQniKVFiW--K|W;-UmRHk?|-s< z+yye660`D$t?}Z9z*fYkrzR(7lM$qD42*F7_4rou=L(1Jd(53cS2xn#-Ms=%l_2h{ zwr*9P;pDV`-kwocS2sL7e0^HdwDfLfW+t!(u#UQyx;QvE1Q7@XFp#ADBS(%j-#FFq zeLA4a)7>3KAP{^>F)@a(GO2@3WDjb7Iec#Nlg#2sT(Od3OojD|*$CyE7Vr+J<-ALL zX&X+%U(LaaEc6awcY$(ZLn4TiQxi`zLGSZFg!_H&;hr%P8TjU<(2t0ScWi}lC z6*#~|F5gh8ZMIWg->6ZJ+j$R|L4fh1R-x`8c8}!ErT2ZCR_1&+H#WAGOD6bDSFvg! z;%R9XL7GZ({<2C=NnMPkFNdltDmpGAo-=G9AS@`eEsFvF#>H)oJ^dNBeN{rp&`O7$ z8hszct>ilX2Wu2guC&W~h+|>p(Gf{6I7|(ny$E5NHAq>Z+A1plY>vAO4QU+;IqKM> zC|u#08OANUV!Tbz6#F4mJ$eJ|fo5hQ-|H?UAw~z9PHF73whwLUm4U!=_SfDyLBZ*! z-lAuLnw=osb$sPw_HMfgpZ8Oj6k-#!9OQyXe=MN>gz zTUNhRa$*s{DOxxie;PY?;xkT!a>>EoKL7Il5Qne-{N>b}7tv7b*qbLJDyoU7T?z3I z2#8BcLaX!ne!Ej=NkoD<8>|belai8}1ToHs4MMvA(n%69 zF)?u&se!}@>yh%jQ`w7VLeodDiyA+NG>Bqo6hg6Xx;C}^KAXe**k4YAEsA4hi_5Fz z93NeWS;HyZ^MG39Ik)wmW)Vs4c5}H%jb={PNgRBg!T~as*vXWG`)BO!dEBc?dEdy)QTdr+ZLDPg z0qxY+p?Ga|xHS09hPrO#`gg6&nvz1ia!_t0$Z{%xKML3weOKH5QCrF#BB0yh4O(d~ z8HqKRmEu(496=^PS~q;D-7Z^-s(S3o88cSc;_u?Ml6d(d5BH9fjr=<7`VCr#Q|@mY zRIC!pk^8Nu+jn-tL0X{M86iH(gFR=Jtj%zPjs}iALun>76Ph*jVYmJJZb%^8HKky* ze>Megd6d`cI#D;fy1J*akCm67g4O!9wcVK9TYi2t-GGhp#OW>D zf(B(n=UtMDthS1H1jRhYDfXixYkFP0w36(8hX6>tb}Uh=2Z!Idd#vI24a&+^D_5o( z^UralW#Wa%9}s5hxp|WgKa>dV0iw;;#6&JZpT2NCM|DVnGZ#{icYj&dy*fZ&)t*r% zdHdIkz~8ev8)r_G{?$O@0Xc9Uw9>f{q{3#0EANN06~YMKtMqn7n&)KGnXtNYJl+I& zvxq$Ihv>$h$h-@JK~Hi#?QIx4}5$iK0- zx2O5&(PL}tEMF*yi5C_YT+!Hs{!jAy(#nc42&CH)PT=gtWOILHask_K29~E{+#m`v zw4{pl<}r2E4iGf!w+-w^rsd_&h~C_r-m}d4?J=+)a*98~KlAlNqLF@GariC#){fVM z88vm=X@W#n4!rN1R-~>sR7UQ~SnWinBRAu-Wy3?OJuxWTqLzN}kW%~Q8-nucKkZNK zFBO19ry-@?U~|-ViWZM2$h-fjd)l)1N>@{}t?~t{iv<56QQ-m4+Jky@3{G)2J(FT4#s^e8R@psC&yIU~mKoaRP4S2;h}Q2A zd_>gM)OI%eZ^SMWq37g!=f5?OK@v`w+^)X;0oF{aiN$6&z%@|~ehNzBCR|+AXxP~O zL!n1#!H$;|+%KsKM_Kw>SxMUNq)bja_q-y;*PrunU+svudhuOUy7%$$5qq`Hhj!D7 z=GUhpB&u$l((t7|I=16?p3y}*hg)8NleW5l9@=M(+7y|(6w|y)0P_ z6%aS#UrO9Lk-G>JhITT6Wy;O{>< zJ6jNZj9nC@c*$}I%Bw@iV$;%G*}`;nb>F{zdqYJT>qJH~A(`{nO*6v7Pc<|&IIfI0 zI8lldeRt)8r2=&syZIM%c7Iny@@n)}_g_!Fos=i3d?$AyDD>$RJvbGBF|kRgP1V*8 zT@W-?OzWNe6U4Z8>>ns`VwHmw&E+MgryCnsM;aKq%LMc|2K@fszMb1%UEOmsiob7F zO&_>$2tW+jmi2D3M=bd3nU(A->4L0BR9JnmPSLEBP04|jR(6SV(?#RQPe&}sk>?u) zyOY*C{^A`@$U3XuTjAsz{+;Odz(paxtSZ^&>CR57=jw|Z@H`IAm|RH}gt ze@>pMZyHf*aah@T^d?mkfEbyOR-1L;Y^SM|@J?NTU&L&Mn6WDxxeG9GX z0+Vjqg!m4=Ja9+S0*)zqG80dTV?lEypK|#Uhkr5cA z==@GY)#k8A)YtlYS-Z9i;7)TtL-{Gdf^4M{0X>n!+~0qGeUqdTL~AO}_wTfpqhQ!o`V?e^wCykA7d+MO})Lxi)oAll;AP^cht=0tM zc-rM9fGTk7$=8TC zg^WTjABp#&)ZY*8`&5L8rgY;H(!u`N{ek3XR1==f7jVUN<+)kX%9bO@lJ>uJH4o%G z)k~mKrN1(vhZ}9I5!a|fFs=X>oC4f6%CP9&QqsU*;cSo-C z`3pRDbgV{B71pklw7vzGQ|p3#AVPgD+e6#|E) z@DULjsRjh#z|ub01$sk8MC6@(>>Y8=CYsgNzPopx8Tkfmt%yYUzeop(+!2Ud_u7PL zYis_q0Q;Xr_Sl&Ggy2Bv`H}&{jwW)ut{2U3c2N)h5n&;yfpOhHn>E6+k1m>6->Xc* z({GO5nZB3XdYiUI6`N14KEnhdHRGNer61UwC$!U*HZgQQERr z^L?Q(8~j(J>;}<^TQ)CC*>5?AZ@JDSaIb^=hQQ-(AK~O}x}8TMh&=A!SM=N<^Vrvyie%+k=e+(-6klT3>%ZPHe2!KDSt0&g6t*YNov5 z*S?~3mCL8^zVo5IsA2f*tDEoZKDNutEa5rJ%miKG&@$Iw2U+fFK+3**pZk*PGg??s za4mvg#d2C@Gn9OwQ1yYAz(?Ad@x3u&SPB9-UA9Z$5?x z=jZ3+;HAE4X=w`H3mh$i_=NlDEDt{}gHxC#-T`@!2()hRR429{?5=$r10oN%~6?xFR2seYQ8Q7*t(GRf{ zH(orPfE`Nrk@=M4=GKkY30>96)1~2%#6F|4k(PkxMYu&{cqiSnd=ql{->%QLj?tua zKTvF}O_I0=nnnMgH0vJi7>g;p4=a;5XW_Yj`tW~te97bdC}B|Hc2V7*(Z1F8W$*~g zH8VbE6PR=wTr!HUcePy+oSoi5-dzqXuOGA>J-wGZd~Rhi`%MSp&Z@bK;zgH5DXivB zS>~#@kEolaF6MG5A>WknO>ASza8ah#k9Te2pIGfh6|Q`_y3xG!%9QIp3d1S9G8r~H zI@^xv0)^gh3{FVQXIkOYE4|m=Lsna!a+n`6db>BzTnK= z=B{~qdd21g3({PEHez&H4Zi+ct_=s)0H+F+HNVM_LQ#OZ=TI z(A-#WR!<<4%T{0z$IljT8uKhSmd*P%sHLu6ZK}UA*<>fjC*4|)0NDYcKuP`hXk}Q9 zx$NtJb&5@UUuED20Y`g>iqU=V{J?(yuY)cXUDyCGC0|5rxLaYFw1Uq% z+p}&uGst+>tU{4ELmi0Oq!_wlGDXBTp@ehg9X>`M1WE?ITLyD2qR%MNoQygd%#LUO z{EP960882*x~$O?T5paH$A=m8l#?q1GcA1s7s;P*!PGgbEgD))}@yt7(&Iyp8;u>OM@&k%*Z?^~w0 zQPDj|wNx!KidCK7QmoU`EW5rH<1#t%vp9DBIJ!KUyL51KvC2`)pzJ zysYChID~!G*xx;mq5a!TI7_ob^&j?7d@MF0%=&CE`ICNW3!}!>8CZ}#w-KoD3?x~2K;w02K@L_fP& zKtxQ@WD|!$gSpF5xPRV>p|>rwR?X(_@w{vCDv7*{DDL@3J*kD-tL7~<7b-`XH+_&L zdPl~D%$J7Ekm5_agD$}(n+cc=RCANdNoiZw!L(Cm1$a(pYJwgm9)f|8u61^n>i%vF?Ncc_^bTr2uD^vQuc#@?aU-vFBuK} zv{L27YMF7XQ=+>nG`Rp4da`ss#(}<BnaQI}gO*39Kwt4;!RpJimMJR@hOw=+0(pV;EM5 zwsjK8JjiH%;~)5D#W8worQu+y+*dR!5wB)D;_K&2=4V8pddmega_^*gZSrz>%G^W2d(?NbcQ0OmH%hz0 zTV61p5SCpQ#1)OGA*MF(Z2yqJ$pTYgz^!rw99L?xk<+AWbSE>i|H$ww!2*~l|vqTZWK%aTNidlABkjfMD z)|x1!*k;yGlw#zAqV{sF_NavYfHwd5nJAe3bB|FkpP8|l z&B_92dU>kPn7-;tYltT5js>sRhD8$J3y(P9#=?dxzkU=@qEyH8A-7FZ6_R~!IWMHN zb9ZN_LCLIrjG#>~k3gl21&jMDB8duaw3J(XZ<#Zv(UW&5x~2qaRD|=QHhK3TVvxt$}%Yuf5`!1kBSN{PfMz`jGoye z9G!6XPf|8pjbh-iuJlW{!M;PJ({FhkkIk`(euspAxy4)=VeD0ujV9P-#(1GO@WiUJ z1qMQ6JK~{PG6?!*c!J}`uT)ubj$O5XZ|cRx_2#z}BzUT<4_y_WAR>a8HGi~aFg>PU zS`qFn>+AlUG$%GXYqv+#e3=mMdfV>i{*O5i^yIQ*;KjY=(@Gkeoh|xy#+bhiqy(o% zJ_ra?KZL~uqmLq6yT9lbswk%+l2vVo#Y1jF>+d7>la{%^tZTPkE_%vYNgwmYbg-R} zn8@t^<|&xFi(i)aWLUgodkr~QTd_YWALe*WFTMPN?DOkiS9j0yC1>5W@_{E{ya@HO zzw1>5rbd3}>g?Vvd6AvW=ReQ6Tej#KJ!pJkzsJ?x%e(aVc233hHw{8XsJeX0@)llF z0y&bdlwcQ_l9(#SnUC^GuRLj0XD|xyC(C0y{<3l*qo{-e^IlCTj-Er>T6;@bl8`_4 zn}x+>CTX8X+>#Tm)Mz6+lMsAl)PJh6UAV6>njZRNQJ$}8%qOi`ZkM{4Pkz!bha69< zS*5$21hT2s&(0#V`QVmN^dbRBpBB)x6~w z+3480QgsEh$VmJ}rrevco#AFU5AeYg9O6GFQ4k0NCNa5T7+Pu1gEwn)kbqB`FF*&af^j>ChYtPBR2E}*hKgREQX{)#@ zXAX4w2pm87S!ZdwM)3`6bF*y4)%EF}z_K;(e;%Ga81}@Jsz4hpMj>UeIZt8p7TYHf zAGlPOsh@R$+2o~PTwGj#97=Pt=+(2@4VD6L7||H%($pVrtrz45Uj_fj^6-87pzMb zT5R4zBbLPC5YNIA`8vKEl7=0PI`uV_Xt33RDXWF{tx(JAqG0vLVWGg-Ubj|>RfQAK zzy0REc$4X-Gf-&Nv|IFGe0;PxFb8In%Cl2@q{<-jpAD7wm8sGui&U}QrNoR?y8UpjEqe$oBCb90Cd`piN$){3MxrCB}KmFPj+S#3EO8hgIs5CYRPqgP38 zKEf`hB&{B_a3?!-bdNmP9{bGKW%A9QoPGJ|4su9v|CWfj7V-OTPG_2Pc0N+BD}?xy z@nl6ef@U^0*e+`H!Wq4kUaC29OtxG2Dpc&wEj2_SQO7Xo_cFa2+2}|vv4Gmw`Duq- zCd$}kld(qbMXiQN@rOO?>X$cflY3olF%&q8G>}wwOV*XVm_x9FW^OS} z(qyTpd+QKKmrtc-VzoxP(!fJ=;mx?P7~-v^D5LF}_$k=7Pei?zRuu`JCtzq2fEbnWkToL(Q3>V$j`n-ci6n$JkxA~~Af@G@AIrSQzo3u&k^ z+~;^8hGqy(u8?yvFIej@Au4C+$eOtAtiu(HF~U%hs^UYAGnKhZ5le}e0011*?#O>q_fDvfSn*z7H~*-%2MG%r!2_O_3jy!n$JdYoRMNIhr~Rkd%=`!I@;8|1cAcd3xdd*0G=6@3<8w^mcz! zRn@)=L31w!SxI`iS{fo);n@E|LLBxlIdB#IIrT9!aV)*b=C`?o^JZm!c*)arM@B|a zP)$9G52CFdE+s8p0^?+!$+&P^UcR}rQxAeRicU^W&It;BqN(}55;2BB(rL{=W6{yk zLBVh^uYG(9@O^-%L5~o8<|7rCJ^(^l8z?!xZ!8Xx^K^LN)ML!csv?>CDp|*#2i{D? z1t_TAdrbj~n#mdB;8a=c{MJl*Li00J^D_fCw_gxRrly*u5~CV?Q~|kOu#lCV-9GW4 zb9;Alj6UqqnaGa^Ot__==jTsV@X`c>D+;Tr0L{()_U(a~${z&5&BpBT7d@3-Bdz%B`pLIkDja)- zTm~yNn3|a~+g9esfuSL*?v`{UO+*$kYN4#G%q{Pp1B7UvtCJcUJ*!i#9K5_G8Zko6 zLqq0#e0+=_!R)agK4fr+8J~k+O^~$00K9k(bk3yEAa@cdSM`*)9(?-r30m!*8<+kE z^Qro@3NylkM}Y?n@4VRG+XWbN2c)DHjPCRppmGGK0-h6N<&312l!%{~uU}vKnHkE? zZ@jfS1*!3LT$8b~vRWH>#GL6;!RbpEKzsOa^po7A0MA0SAOO_TxCEmYhGClq(wPwyb2BjWwxuUt3B*0 zI~jF*46WtbHQi5$DxjWz9~nsjDm}<2Yqhm?o5sjO%{bA9ni3#TXO1`M<0_pVCjf|| zx%IhTv|0Hyoo3E_z_sY>m@_h`?!SBY4%`?e;4(%U@!#wkr{rpJf`z43AIfg>;K4sV zJw59{M_2@ybihD?#u)AUsX54IYHJY7KiXm%dC>%sl?-{KrSz;r>7_D)c5GX zfddR+c66}P#4cUBi;{62zFl2Y!>nHDyt5Ym_3PKRsBQYZ$)s&cYAVjU;Y6yEpD|z@ z41ffgY4YSElh*F;?!ivx;$|6soR=Q7H!wR~U5=Y)e!()#0)&*JzfVs6IggikeTn&P z|Aqgrk3~nn%%lfd3dLxEzhAz5QDYww=zx5Zh8=7}Qd8%*L-w*&d5kX8aeaZNbg$4hs$$zFmPH>Iu%UIAp4Ag;WYH79N zA0v@93yX_5yLN7kp3M89eK@!YQ?Ulz>`0AQFPN?Cdv-vCP8+MM^Ije;nQl*R1=S6p z)&mH}fr6*Xbwokdq3iC_(ozkOQ{V!&H|`~Pd3y_;Vj9(F+mFFdz%q;lo=>t%qvuQT zF(yA+<25fTEo}&B_qeF2DBVPQWw6H2CDVLeUS4H?Q)ayc6$6WliOB}q zd)nt$htTHuC0mneP}H;1KQLgHs~J~A8$VV57|634WE2Ts5uVUI?L`09KP%$;)XCRj zoRUn2V4YTk6%`ZH0wNd+pxFAd)!8)+9zA-?>;)KTFp`=mrppC0K`u5cHw`<<#%2r( z4^Sx7F}-a zC!0$nOvk0PyWXn_%6be8P!FT{O27t!2HjE{b@TSF?CI@|1JuRBg0qH{N=HizN?^o` z3Udd=Mpe%zm_8mXBggOITyHQQfC-0^{Hyb@g{(E<|RdRK6llto?s6qL9SVQy|^fMSVCqPJ2mJFGbbgf%kWq+rf zSs(HqiZ=5%3n25_1R;HB=J_U{h=u#QpK8?^VH`3WA^t`%XS)YT@eY>Z|$~#jMuk z=0*cYis54NWiQSD0l|GstVlN4L7G8ugW5a!n%Pd4q0dxx=J933_dG9(L9}_ z19y*Td!3FA73Hjq7etO@`2+Sg1ma5{S6R%~111e~#d`Lt$o076) z1j?Y?sO~*BPi07TbV-+cwVNA`pRf7#+@~k6lhFilRmsl&0;2Hvq}8C#)#bwci1ZEp zqWn6lN~3kjpQ>0Eiyp|4FVnc)xRm(Zl{I`){_ZC+ZT8w~O@N&x=Cdvs8A@&vVBiCd zi6=8$6O>_<=CEX=zD=x;e&V*>H9;kFu;0=OW|xo6kA702CAj3%jj#-4mbwz;*IL)R z0s$piU!w8yZ5MI}vLwhRZo^YQLY>r8J$Xu_hZOUi5irB06O>1!K{m*sS+)rKo*}>w z-RE^;f_$N!-{uD->wNaqvZS}7aCh`qg zP%$|)W?r%xQrQJqw}ey;#?SU8h4aw`C)nE|xX>A~g78oPF~Z4#Oxtnus77C^qW>~e z+G|cnu65ffY>(JLZ)PT^)zxJJYBecA{_psVjAQojsl2l-Z5`ynk)I0wLY8>ywW8t_ z3-RskkI2emXG@Y#ktmZ+1qd(qAg(fRze-Q>UUpmz+yVR!3W%vg%cnkBswd{W4@^_5 zVRKu;9)sBbI>^fwW&#$xU5P?QPnrbUUG)QOZ}VpATTPU}?x*>1f1<_Y%mg~`@*bLR zvV=*ae*EU?($AB*EnIBy%mNLi{Ph^Xo)L+ActH4a#C*g&U#px2GWWcs^i`^U01)}P^ps3izpDZoU~M9u zR*?ZU&UoB*W7`K$nJcQSMe(xi2Z$d4FkI&DE~sIc?ur#XVCh6(-()h^2p{1FNgiZ3 zBm6bq=8<>a>w3HYi%vMwfq0x7jwAeW**ZVa-_I%s1>INb&UPiuC9^$f8SM~YH&MqG zOkl}Fddc@Qq6jfghqpc^w0Fo>EMRSD-2fm5yrx=ENK&>w0QPAnD>zdhz)26l_MQ!U z(MM{s0VN*Ul0Yy0=!%lr&(&o-G1byA2vgWgTAM$Ry0uws)%(F+ZS;Hh+rwMP>Vjf? zm@hP)0wEXbm)@&CYt+&^V7fG$JBaA({e^!QYnY7`mEaoCW?CmUH zAL10-q=9wTm;3yPe1BqJjqA(3L`DXbda}aFo>=Zi&{Y?P&rfdC0b2&iw|4Y$xR4__ zQBx35|3qi|dAh*d10xhzj|qUoj6lbCpH-rXiYZdF7T*l`U(!6Lo67VnqFq#-653G* zz|c-ULF6y0XW{5wW&sLiIDaJ=pe}Nnvj?5J;i)BqlFS9;Dq$ik<4KV`78{>fBD`FJ z-Dh>Ut|zCi_AQS#QzA^SlTQ}ZNEaU|G4?uijx?+PrXgn*_|c}i*konifrs`JhV(Dv zFouo&sE)ySjC94KTZQkgii><?woX?_vB z{ibJ+%0lis`lt!#%x$LTlb!pld8670TdgMz_j^(}6-A$JJzhK!(Vfk|gQ?0n6|h}L z@Y>W8lc;9BZ$LFMaasyX_4FLAe;=+zhL_7FV$b^jXooO%nkyBme>}~#pPr3MwO{WV zs)ORJ{$(55P8;aUQ7mxk^IZY;rvBe~w%N&bd4Klrm;bhZk1PH`P(Ob^{U_r3e}C-X z+D2+HG42UQQ#+~-YeH(qdxAja^1sIq)Dqjbv2m`83Ye$`PcKJ(>Hi@}DD$&%@Y||M z&S}drpSflkzn4A#kt|*O`Bra zYXDp*y}rc!X85?x_wa7NWupC}lnhY7{!8}uvgBtt^YlxerBvVf^?`An_ir^SyhWH% z-TJe1JEZMn9(aQLh`j%Hl+1?7+~f5rPK=pxl3PL_{uQqEp?(0JY{E=r0sM9nmIp#h zP#W9|0E}YTl1}Iz@#d_3{}szk7FSzDCoF%(O7}S7= zXu+JOl}M%DqKdg;n?&O?HkK@$@bKsa3CvuqhQh7go1OYiONWqturc56f^ZsQOq zyuUP4d4Fe3YcEjg+xPEiP_PLFP`XzD_WaCtMu8UwXe9x3e56A$aHnc#ch?JmPhx}K z8vS(^9VIoL(K!bW9i3fi!USV$d@bdXST4~>lx8<95GY_IXWFNYW{2AqT=&~7&v43}hSCrUXDl;z@DtDFG6 zk)-5T$>bv1Q{;OGr_P}PQUj3rNP)TKx_#1;YH3wfbv2XD*b00YdC_sP$5q6<(pewW zy>oDJ6#`rts_4DYb&TIX1E4X6;^vjtSJW3Vc&0d9RaKR0JEG{b)CX|uXM3YLC&!9R zvH^s7gk--|HNhr-`*s09mq1Bo-E<8W*RjTYDF*W;7ocDlw&F$O_yrL8@rJN99jnqO zuKg@*F?tV`zhv+K~V@LMcyMeCdTIGKIph{@KILq z+8&JJa;>4hzCIvQ<8Lb}&Vzg5O-X8D=aE}e34$7HGo2=2I+(@%pmlS-dMdJ#Ky=?Z z7p>0M?a`@}4_1QcwQEf~KB(A~lwu&cFYuh}0rXKXQ18b^M_(3MF75{ZH3DEINOFrn zv6MpK&Tm^dsefwFZRJBLsB|d;gp;UYUN~?HGS8Y$=jP>!%E%Z2fOxw6EO-|XsEMvP zewbMu3Q#|Gc;K4V-IV>8=l}jYi%Z(>W5O802Mbp9_{4-{ z^(e52*^MDY0$>Zbe4E?aY8O0=A6r?_e5;3dT8Zk2*fUoj7Jydn{FYEjgZ1!YD|umiN+0ALof&&MCEjakn5 zE!Cb34Y6F34+{@B059wtSB3Jjvh{Cg^$IMi-KS@~0Y5mMbn)WF3P8m`N}v$H!qU34Lw%J1lgWMOMJWl? z0?)7Xkct703}m9DvX7t+N*+`ce;>bO3ovPb-DU#w5ClsK$06EUv9QD%! z!#r4S-)U>LnJ`!>5U|p~$&{&sT6WrS=%LcYiol*8vs!O5ljR@5XtbWjS8p;A|O!&Bne2)U;?!P(Q7~?NfOB* zIol#ha=c^^l_)vqS=)ZUA2U^7)%=*RW@={sbeAIcp0m&1PgrX`>$t2f4rJJms6lHP z8HueZNh}Mn-y6tzdbHz^84z|#6ycRNGqN|;gh*yMPv3ywhhBfUOs&jv!rWL_JZ^v& z(~KZe>WH)-v)YO+ZmE?SUrr(V4tE#i#i_ zXGvyt=5lq2q`FG?gNpB!0VBk63mi#?K^a3yX=%Tf5@?zk+;yyS2zX(0$d*y4smO8 z9_)v=NG1bUw@(|NFtQ+ndVul+POsbu9nJUo#58iyKV zn0D@rG7fbx?S|(>6Mk|ki;0~*3H}i#7|EA#TS#HpD|U&mxQ>l&I?4EzjC-!f;&&D=Z*Sye z&8438HOG>UQ2g!M(ulw+C~yp^)_wJtO?trYat<4lGPPFAfA#&J;N2ULAtSK}S_v^o zf=j3zz`S@=kLIc%3pV4a+Vy?x4fc49;3BnHUn^N0!FqK3&#YqMx|ZnOum&rU0Rv0C2T)3c=5`Htsng6M=~3S zTMxIcw6@h>gqpr7>2J?SHSLfe%?U4y)U0GJMs1R>V$w!idrfkWF{aDL$CGhGNBH>8 zBXyc}W~VwACMPFjmtU_rMg4e@viC<+J^ukcQPa^K4c^;St~w5d-7+ojt4#g|2f9oD z7Bpa&k%!EDdJA|(E4j00e?BK4AD`DJEaI@+Yo&@1FPW)z#DR;GlQT}Qs4z}FISEYC z>|p(AERb18W+LX#!?EH)0m&$!7C%-a)E)upy_sR}B?O1X(+VYjF_F|lP6smiWqf&m zO-MYJl}AwU5^A&EvL5;SxwtZrZ7OkU6>(}kS7$M3D1Kdra`o+bRzDC~bj*C@vSBTR zll57#8)@bWD#V>^Y!))hY5K*#!Avh!OxICvJJuDN2{MaBi6VA;$P{S{6Sf)dZmF2! zQ$T=Lg)+j($|@N-&-~NFZT*awq1vcNhp}_}_WjT=kg0m+vvvYTa5W>r3ZvFX&YnF> z_Jvt{dMt8^neN*QK_`am!tWse|v=MpxQ0EW2?*6qCM~MW5QOGU~lv7+=Wo7 zw!Y$<*qtsjgW<@HI>--JcRCPa1E7)V?CK&$W?=3#xAG?F7UY7PzA;vdfNKI_hx$rr z-t&*4X_&M{$Wag|T!_H;T>%t8GI{vvxoAO&r^)F>k4iE(P}9;`12)tKEPX60bziEkSS zTE3*^`H6|Tcgm4f#*WeXPv%if#iIy!!8*|;{B0;?3J#R)8*OcERqQSMvCcL(!7-av z*VOy>H?EF*u3x@&D`jhO_P3EWn-1OghL7NUb8{_tvem$!@T8(E&c8evsP+7)ni?F# zv+%v^e!Sc$jfL!X{$jzMgF#sWR#!wX#nc=mD9lU$@iu--c>BKF2%rdyXzd zB%c~!s4HY$ZCdN0Y0LHwn>#-eQ+p<}o@0Ez>9N34oW0J7r-d9;MZ}mVM9Nu3rTg6j zNV$-vTHHQHqDE!*{5mocG`?@TIptZ@5AC6vp@y}?6=hfA`gjk#Rh+MR+VEbWh=Pzi za%VI96If+&n_^EBxeo|mUU@=4<4MBd%5*IVVfD4=cw1Tweq_$zi0ynVOdc)(ROFOs zZ1YNpAu!xe2>lY}W}jqpYiny|XuIGCK&ob*D{-#|L~gcTa_q7XfKq|5)hNpw-6A(^ z-n^Np70~7dBraYLFe~<+9jF5mH zlnO6UIh-36LjX0SX6}{wWBc~&*7x%P28br;2|TAz_HW+&z)co6nlo%KAtpA6T-uC0 zWM9VSIG91z7503U*YvaU9Yf{o7CSLN=x5`x#rmW%3$2ZQ5BjaZdC>sZb#`f&rar6a z{M9du`gYJb3^&I3Lt+LI&%0AuvJO8q+~GnNr=9aY)T!@xcp8S+EpZ<~*g`G$A14b% z8iw6F`b2ELv^DvJa147)%BIp`c`C}v4)tWxPNQdGS_nO2a{oh#MUGRKT-O|mAR=~n z^JX8(>|_XdYjKK3tyl2@0k^JmLYspJ2e0SO&M~$wt;k2sh3>A?>r-uM#z9o~oV9{i z*g|*jP1o$&`Q6?#)J)J~%0W*d{Fq7- zXP2mw1gqyF(|s?mp#VLPdI&Yp>vQ5)5##0I-uhb9Dl{seD~W~0^^2^gfPeszQy@qO zB66fVjB1iag~w&%+aaAi?I83+h$ziyq$dh7$t5}fF15I)L zzf(`&=u4g}JUu7qi!T9AK=MCJOjXeDwP#yLYNnfnGxKYjtS-%xQW0Vht-);#Bl^e_ z5;or+F6PY6&R!wvAK&qe8S9gk^TW-&J?{tnpJ)%+s_bM5RnBjMPb(2*poX4W-RaOj zufp;8=y8`B6A~hnq$ckZg%Sd|8IopzTMg~7Po=a#K#)MJhJzZlZ>4^fZv3NtcmiLg zq&}0{`1kIjgU^O2tGhWk1fbnR!DLn+A?J0Jiheyr%=#3E`^;0QPn#jGn!Xx;ceuKu z;%S9pZr^-o^v;&e1A3kVj4Se9jKA}LUOFn8ybp)>=!CSb{u|ygivV8riv)E*CL{8e zf`vXG;#0HevJjC__Imstip?WXDxo88{OCnT@|Tv>1C{O&9mmo3Xyn*Ir~y&f-71{j znI0x(-FF}9*|s9jf2<=*3BX$mKZF?bUwGJGc&ntotcK(PaR#S zXSC?qcg}Nd$J>m&;-*6V?mGnx^!NC^z1Gqfqf~^a80NZ<^=a{IKg(KQGN7_v`F%o2 zfCYR;SXO`6g5m63BPgNB_uWV3;)Yx%kxe4xe2A*=;lnMYo`K4fmVx0!*hKcizCIih zA2*?IDtv3MXJgJK7CrHHKeVd$sh*dZ7Q!+&gMf0L<$ybnJ@ESQ;a@1}dBnxrRvh7* z@m@m-o>D|_m8g+=3f=*i@CygN)AJHg3A;}iPJU6eO9QG;Zr`RsSh%`%x-Hi}9yX}4 zfUbZR2=~}C4AlO?6y5K`)Z$HtrtAW;n{dy<$n{uNzG?A8xryO_- z>2uI0CoL>};FT`6pPU+@Caas6yd(9o&%hkqeH(Gus;Ejj791>1q5$VoUokT=8OfOK z*|p1LG7I;vaQ-}>O~=u~cO)25kFN}9F3(FzN=h~-YKivv2dH;HCVGIrhbvwi;sw3+ z55t-W=b;h*nCKNrFOSrp|IOAxq_*;Vzg~Rb-~hrIQ77#D&6ZH#B9m!tp#$I zTss=2G+@wr#2A*e0Cd_nMp_biAo?Zf+4SF4bXL#p_*Y$XsH@*I-6z+X<>l6bH}o!P zV7OJ{I4!2u?w#U!-Y~~iPk7DIJ--1NP^Jfq^IFHasJ`ZDFMPb?*XW85mt%aUIF_KY-K zYpD1$ZnmiaW*|-b3mgo{#rv0yt;Y-IQP`eFN%68IFdd+qDG&^77gZ;xY;T~khzKl8 z8$L%^J2?F8Q@Gzd3xZB%CJR5c~^{8blNem9AXe;&m^S z_rSjLh!r6ae$ux8{`>EwBWHK&-7qbLI--tvRRbTH?)lM`r;er1ix~N}tFvRfjGaC-xJNHWVmgiAhsGC}Ml z#qMJ6Wt`B!^9QBpS`APNyIp#gnvXEdG@|7CcXcs>qdNmY@t&}`XMB%4{=*+JUeY`G-B#(_m3+Z}Bo z@1a9y5n%#gASr%a2PLK}AbaiJ4Il4n`>$5Q-Z}l&ms3DZiAm66zj~ai{2ozDRp4U8 zEeIXQN4L`s*jL9m(W*@|~ot z#^>6yE7PTR4JrQAcSG;Sn_O3vm`7%3ZR!^_2K!6|99hnm9AH}gFgx2SilX_f`_fE3 zR*ZOONzc@sCbf$AW+}8sL?BC~`y{ar)`gRP4<;~4Ibm!EI}iA+UXf6}U&#z&>EMbZ z>r=I4-9&W8(g<>kFX0-z;_Pczppme{^P-XDwjP9U#p#~+lb#}inhtCax) zS=n#go8Bq6r|94N<=0;c$U@+E#OdAAF6kvD!(NN9^ql$jOeqc@0qK`on2bA7mCHNp zQ9^Ncfs-789vRuEjzoUr>qph6OK8oDOHi&v9SU<(K}-A`ojp2$H=F2~Uk$+OT^WE_ z^Znm1rBmIMv=u`O_z(0Jbfm zR&C02v?2;%!UU|gvpdc&nlu&-KyjAC_fJxaMt6}Q{`NMepS$9A4gAtNEBVi8G+MJ1ymePPJ|Y{ zXgkb{-vTm>$0*R}sw!1bi9kd1_A^$)=FJbKr@pmuwqg$ZasV2-cmUUcsL$S+M*MIm zpO2)cJELkIemk-fu7Kh(LOmDDAqwk%wo)ADf!2GjV2^K?^$9BEQavFix1 zOSk|6q?4@vcCDc4XWtAsT4rV9G=Vuuj264~hQ#Q?Q@^B;K{d}G5|Q-xJE> zqho@8V^{QePcXZ6<%RjjW1!idZ~sY5-9KWtH!>EgXg*@~=^;kmmeOP6N=6wQI_Nzoo z-AcfiYKL)vSyyO&J#CNQXu=gOCDYPcXGgQpyY*~Yrqw;c3y&3+Ud@{WHACQi?=KWC!<%kttsT-J%?kkLsEw`WOIUX5Kr@+j ze>1z&zp&a%OC!l-(tyxyAncCL_+EpvH5qQW5GJs&@4HC8`@w|wPEsa)LbX>?Y--A5t>%H}C|rqaYQ@K6McjY0g_IW!gr{{L|B zwvD%g$`N0#k8TGhsTY-(pj%F*ssrqP?=U2)4dH(FdWQ&DI3iW4;^g&^Yb|#XMI6Y#n0y&df6Q{ z@+c#Z&Hs6q$0Y*}^O)dH?F)K^7q%Tc2Ga<>H@*ShQmraGyd6>;j!@Zp{ux*lE&m_2 z3$448wh@&l%^m}5ziWnuQK>W5K)mDx@YW#|o%qY@_E`w( zbjXky@WT5iX>MOg^T9|`h2j2gDByalzVld;D7Q!r~$T z->P;aT#1MS8G4$T$&Ztj0jR_7HgOW86BkJ!N3;Zs91B=NGf#(^csrviWD*<4;{yxN zEUT-lYu25gLlE|98+e;#gC@hUhkf)4`o!YDzQDof0M5<93-4 zHz#gtgjXVDofgQSxXV3+Lr)+Hy%3w>J5@)0pN6!@eksU2NYip-eZ)x zgsO@Zp$NW;n2jRKzBrCJPSVB9j9qn@tRDUvj8~@kk6>79;}0k))yPyLpj!C`Qu8P2 zNKTIyu^iPl(KSXt#qud9}^3KNDY6%(yj<3Yfm5|W;4|5wgZZs)> z!bxgc5Hv9uf;$584t*=ntwqc%*^jhbz~s}^#Ka|x;6UDO7Fy&wZ)uP6pu4-9O!eqG ze`kVU@;9wotR3c;#0gM{-M7gy9$B1}G9)vThp!BN8{<rN6bpAV2bYFz@1P>!3_ z?aRr|#tVO{Bly9%5lcO!=14Tb3Y6iN#CTGF%R?LjXy+jJSCm1evR#hPhc(Jil|X%G&arW_;l1nJlvyD7e)s0?5?=*`WwZ{LRn(9gc8;8?e9 z+vd$(Tv=J^G?p{qiy*NZlZ9tM!uw%RXbO!{hvJKhimIKABlq;llWR?hTGV$i5Z%Vi{4#mHhKhz*%V$U@ z*X|fME26VN3L_OSb9io z4V)FbP#MZucOSEup$=m>~%NdZZu9M^&&Jnjliz3v;ugC`ry?ynFX9>;!pqnq8R>U!-i6_r?>L zL}b>?3y!zRSYOA00h6}D-P0O)p;xF2Mj_ z9rgC@Zm4QXp>(1zE?>TFB^2n3cXbsDZvBcj^f-6m=|^ONfVi`0d|gbjpY(@iajHrE z{%ISQ-~NUqh?U|CLZeyA6B)pp`zr|7LScy`V%00E#LOd4#@0LP?lqlsBdZc{X41amqdk0eb@2%K3Fai=qQ21ary zBeG<076(g#Lgu!C!VQ7`x&#}AGrqg@Q-5H{lUtr_RwayXo~Tv4c=07Sg0&(A&$0<+ zfR8=*fdi%x^kBBT1yw&$PMtFE!0RqhvfcF1M*c!SsKmgqF73W)Rvl3d@6Vi z0L+QRsQMrgXMr9g>LH-L(}J~hI%d)+PlTwM7&9hfmpgavkO_T)4zPNMLs-C^TZ5b5 zxtNpFy691GOBzv@5{3I9VkW`~Y%Rv*xsg#|0Eo>?2)_r&j5KMQaYXPM0VCc1m8k(M zGut&yLgYYM<>g+4@hkG?96)O?^mlzOFHdP*`#u~H1!zuz0=W|2lV>m~QFv<+%&3k% z$1ie(#dRyrqobgRJZ4Zjh@?@E(2%saGy&NcfxQBBlF(JRzc=zB()`;&4OjHIoCZQ1 z6)20|90Ts)n~2JYSW1XXNWiE%q>VPWBf6kq^*~Grqc$nJP%1`4dO+lgFwH%aYX2v? z*=9_L0=>G5?{9!g;*dO2qH>`TZcpII`0I@6nswdqQ=5QTo}++Xgq0JMEyl){FqL0j znU|A8WI5P+4Mfj}BJ`AbJkSWT_|b|1!{k{2+eW&#ZCfL$mRtcb#lRKpmau!rP5gT? z3El{z2U^(^1j2crR=KW{-QqxQ4?d1NeT^UbmOz?a@gmW{6rctjAPb}nPc)|6_BI4c%nf>$wRbUC|>{=z94uL zUSL$#SEZ;-2r!r=G*b>JBG(-u_dDE`mj&kz|NW~4ct;B%ZBab1xa3VBOA~DoB(`ZdV z?G;x+!WB&}uCG@n7zz%=P9~;k#ORE=rAo{NA}Ge8Zy0V(jU)-8t^4QvF_C|O_;E+i z>C$^=bCq7G8*Z}#d_#UUN^ad|7xJK>EYyYmj`S)3-M+!}0~ZvJdbR)84J=u56bO~7 z8cDS~UbtMd(k`~1ox9q0)V_qwBwPVUo$?nT68CQJJllJs8@|Z z7=Sb2BtwvlJq0=Xm_s%RT~iz~Ie|=(j%ShUh9muDat(`&4Gmv;UJMosC$Fo62I4Ot zpR{bwDx}s(h>1p!znXCA&^B!*D$~O?^Boz%%A5X*dvnh&paCQh&hgyqEY~k45BfCl zym$6$COs7cEp`k=BdOQiy4o>^?T2NV7QzLHAoW&>dJduT`3EtqA>w(RE#ai~yT6SbsleG2) zT7!+7Hn9>K48T5Z!u-mG3pG6}{eA7UxkI0wH_e9Qdsh~x{jfh-q3k(>hPGPtU*gG* zltbRjfkczK#n}$INUH-*sNDY`hUDq;zZZR4F z2=~VJMXI7VK+i*R9J&xcG>yB_d7Xg*)EB{s6&&JmNI`s&`S|rZaBaR2?bY3~wG~F% zF@y<8)hu9%Ov?CnKxlSX^d>eLMgL4b^+_PtY8_4Qkk2|9T#8u?Dv zcVts|zu@XIg1*ny4#U0@^{)eX-Y=!ieV4}pN<5I>PnMO5>JefI9ir_iSLTv=a5 z(z8d6IrQjIC^_Jk=a%n$4CV;EiDn$>h`I7f-5v`pW#2jn%?CA|*}6-;W@zAvUW$nL z5L}H$ZH}6PzX4+TGa@5jj^GfG_eTLxGkoDf-Zp}&W-aO5hjwVDQKHLsJ?DgFl-Ail zKGBP!7F%3c7>tbxeeZB2fDDF6BM>b?9Yl@??9)v_QU|Q|Esy9U{gPIWD2af2Ud=P7 zf%SiHie9O`8u$W>6di)Op`$G8c-r1i%S|k6=j{P6tW%SR8+kftT;xrK`8@X zaIA3FzZ8O;%Y}uOak>TRE^}M!>%y}t8yknbGk|{H0-SW#ye0UOhDJo+*G8xY6KuYB z0^4`)azHia=74GM-j>sU+^@E^c~1@V46Q4e_lW%TY3uY%QxhNgiV1u1g?(1u?>;0GSjhfEb4l72~!t=3b==DbIh{r|3-SF@f=SPoNhr3*# z%5BMSr5xBivpAqd8$91?P$^W*Ss7{afVh=@?CqCkdFtEt*N%mMS4&axS`@jxSpF7nf z&Afls)XXiu?QzihSD|S|MPPrAs1-w{=z5oKv2 zQ&txJ!3b;IlyEi0V@vq{+h=$ zA>@x@Kc_np-M%7qrH038Q%^jxsZUq?ai+I)(}Tch{@?3g(fHiAWZP=Oq+drXR$m|( zIy!XNKD?@}W_q!+pjK+YJi5dOb1p&{+!Kmr=xq%B8{9;vWQ0{Pr zv7Ho?W6`l2dy;x@b5C-I%R!|i_xOPx*X1FO^@VPEJw}=jbyZtKhH^wVq`Y2N7G-xus1tvJxKcf8B>FdfU9#PYo7(#9Htvfrkkh25+auKiKo zUGSiRN)78-n=5K)cd3_k<4?P>s#v%>n%wN0s4*FKHS>pToYhqnwvks0-E3b9^k@r@ z2lWKM&apk@K4iXuW}>#b__G>m0@+sc!Lq@c+Jvm-t>#)<3`tdovPI@ za#wKqcit*xKRlfl)%6A%EW^-Z?{LwFhNc}@m;j&>MPlmFVzWJ^H8-ej{oS9eRAi{S5r&& z5FY^<0loF5=*1ex8~yF(hgz2BHqcz!Ob@}$is9dPH_hs9rr8<~+`ptpmPX-wA~B)y z+D0sB{%y?7yHtR;FO^_%U~>8@_s5o{WY`n@YqgbB1BZQy zq1gXJ|E+q+@OOt*k@V>3=%EnX5+-#+=n|X1lKHXmLUb*?VQu&qmz85xg#c!`5c}3% z-)(n2BUNIfRAQT4S@CF%t5P2H0tptnQS~Yq{i}onrm~{K!#RO{#yuv^bf%V;HNCyp zfN+FDQX+u1Ss_}b;#w6i-W4|{r8Gfv@CND@bmMx zLB9^!T|i8z3lCtOcY%lgI8dOFkpjk{A9n(r1W0LbZ=8zd83?w$NF;PXU=R5L6QC02*%r)-oulIb zgn?Difg&or&IcPJGJSs^4UJ$!^LOF978VvmBO{7aLzcb3r$e9$GTQ=OvjDI#C6Jc7 zFfhQH_UDcy#fz8WGp7IE%<&Bh{p82hfx-iH$vFUceAy&VK-}(Ze-rN}4-~V$UY!q+ z!&zf1z9gdgz{ZY_4)asDfY|2rzc#l)b3QXakICRe0ope57)lHU{>)wBJP@L8`xt=k z0ePa`Vl%h2T>EGaq5?D5zN|tXVEkJ4c93&*CbzUvwq(7_Pqh0LK*K|T^eZrh+R)tm z;_chFRj);OfxbZHE|2S{to!BF-klH*V+-S3TWbdj`5>4}p4g7rx`AvTNJx`9&qPDB zD`!|mibea@Nhp~E@iup3%xMOSTIo^@0UF|?Vm)L8GZ-9< zd#riQt7BY;E(VtatcW*AE|}9~V3*PmwgUbhC}D3VZApjDKjmFVi@KJU7Dg`~X*;wT z!IfBk%5;lmYYHbg5JmiiVhI0%tALt`fIlCvS9AXAC;A=`h7?q;SKk{}JbCcZ?J4LK zaZGtuV$3}aaNo?_+}pG?VQ@A2)%n0gjiD)<&E+TONW1G4+0?76tAVnf9`-lKI&+4` z$Cb>@&1*m-4m)8s3`4Cob`B0VumzogmQR29mGK*tOG^z@b5qj_z(Zlct0nP(tD&30 z6;RHHNQgk?Wo_RLvU&lM7jMk>-Daa4au59?4twX&oUJ|-+InsdjM7eAwsm%I39(y42A F{~MIDfl>ef diff --git a/timApp/tests/browser/expected_screenshots/questions/qst_matrix-checkbox_answered.png b/timApp/tests/browser/expected_screenshots/questions/qst_matrix-checkbox_answered.png index b28825d1126e74dbda138d3df0268dac954120fd..9588a9d75a9b254520feaeb8d6e4c1c42c26440b 100644 GIT binary patch delta 99 zcmaEPi}B?x#tD_2>{7gZe?nyZD3$!VDL9w#*~49 mLAAs+q9i4;B-JW6KP5A*lEKKp$XM3^i-sPqC7qLG14|Lure^HIr-CXvX6`u PHaXrYj-Mvi%BTVWky#d^ delta 76 zcmZ2vxyW)tB_{_nyPAyofi=+^n>I^}Tj&}XhZvY!8CzHxS!f#=SQ!{hX5=oK>?0$E OP3}?5=5v#4WmEw+`W0dT diff --git a/timApp/tests/browser/expected_screenshots/timtable/timTableAllStyles.png b/timApp/tests/browser/expected_screenshots/timtable/timTableAllStyles.png index 46e6cd0804cd8d5b0b467b2a8da8b0b8468827b1..e309ade279ed798f2bbe79277c5fad0596009f4c 100644 GIT binary patch delta 99 zcmZ4elX3M=#tD_2>{7fEl1p}a?%vq6tJ=d{*T^iy(Adhz#LCE2+rYrez+k2ZD>DNF ngKCLuL`h0wNvc(DeoAIqC4-THk+H4;77Z^-_wJwEQlkz45)2+I delta 99 zcmZ4elX3M=#tD_29L(%$mNiEzWHvVKs`jwZH82h_Ft;+curf8*HZZUDY diff --git a/timApp/tests/browser/expected_screenshots/timtable/timTableExtraStyles.png b/timApp/tests/browser/expected_screenshots/timtable/timTableExtraStyles.png index 529bbe50da65f139946976cef56a9f767b658c2e..0006b9e2bbd3710a6a5ddf0f3a02655db87c19d3 100644 GIT binary patch delta 78 zcmZ4di*fNU#tD_2>{7fE+Bta=)*G9QYQ)WTjm$y}jjfDKtW1ox4GgRd3~Z)b?wfqN QMhcsp@2N(n$$x6p0jAj(S^xk5 delta 78 zcmZ4di*fNU#tD_29L((Me0P)H1Z`|Gsu8!)H82h_Ft;+cv@$f-HZZUzm7AZEnO4bQWME{hYk)!gc+CU0d@1psy}9Ap3h delta 97 zcmcb`dW&^JDJKUrySmc*T@4#I*1c!+u+TLy4lyvdGPblbHr6&Uure^%lxL#Bz`&qd l;u=wsl30>zm7AZEnO4bQWME{hYk)<=VdI`DleaRd0syP;9CQEx diff --git a/timApp/tests/browser/expected_screenshots/timtable/timTableTableStyles.png b/timApp/tests/browser/expected_screenshots/timtable/timTableTableStyles.png index 61e40a870bd07f07d2dac601b53dde11317ec3c7..fd13b7bf9dd24182bfacd92b84f115d4a0f06348 100644 GIT binary patch delta 78 zcmcb#l=0G1#tD_2>{7fEa#nZmNNj9+6ew=4Yh)H;Xl!LK4JUTZFe PP44N(15YR4Ggk!wsL>ef delta 76 zcmZoJXfBvg$;rXYu3<2H_FJ=!O(N#v7Pzm7AZEnO4bQWME{hYk)OqY^pc(u+TLy4lyvdGBL0+vCuX!ure^nZwhZ>U|>)! kag8WRNi0dV%FR#7OsixtGB7gMHNc`l?*4nb$(}~401@{ce*gdg diff --git a/timApp/tests/browser/expected_screenshots/velps/filtered_canvas.png b/timApp/tests/browser/expected_screenshots/velps/filtered_canvas.png index 30a752cd47dfb727d0d03292ecf60d5976bf608a..a41defa4a25ea29fd75707bb82dc82cb1fc39cab 100644 GIT binary patch delta 99 zcmZ1{zD|6@MkY>nDSlz*ovZhzY~IDx&f#IMYh)H;Xli9>X=PxnZD3$!U{IpMs=>g( mpjzS@QIe8al4_NkpOTqY$zWt)WUOm|MFVT)-YJtixm5vc>l*F= delta 99 zcmZ1{zD|6@MkY=UW_As|#3>g-H}7I<=kT!5H82h_Ft;)>ure{#HZZU!17#LJb kTq8e%5_LB`3QSuQ-c?y5-u9O?Q<%%yo^-LJWzm7AZEnO4bQWME{hYk)<=yWgw$Cofl40|0Vw9CQEx delta 97 zcmX@>e%5_LB_{_nyN2RN?XyQVHr-Y7u+TLy4lyvdGBL0+Hr6&Uure@6ci>87U|>)! kag8WRNi0dV%FR#7OsixtGB7gMHNc`lOm@@V$;*}10Ien*4gdfE diff --git a/timApp/tests/browser/expected_screenshots/velps/velp_menu_advanced_controls.png b/timApp/tests/browser/expected_screenshots/velps/velp_menu_advanced_controls.png index a92009b66b2648981edc9b5c38a32c477a795ac3..11866d2eb9de873e632b76874f737dac6073351f 100644 GIT binary patch delta 76 zcmccOf5m@7B`3QSuQ>BP=QVsAo1UtPo9h~xg%}!J85vs{7;76CSQ!{ZG3wY(u2Yx7 OCZ`=UrE&6lbyWaYcNFpf delta 76 zcmccOf5m@7B_{_nyM{uW&flPoO;6RtEp!cxLk!HVObo1yjkFC6tPBhenloOVT&FID OO>RTh&dkZ{)l~tEh8ENS diff --git a/timApp/tests/browser/test_velps.py b/timApp/tests/browser/test_velps.py index bbb7248d7c..58d7b6b79f 100644 --- a/timApp/tests/browser/test_velps.py +++ b/timApp/tests/browser/test_velps.py @@ -240,7 +240,15 @@ def test_velp_user_filtering(self): "select[title='List of reviewers']" ) velper_selector_dropdown = Select(velper_selector_element) - velper_selector_dropdown.select_by_index(1) + tu2_index = next( + ( + i + for i, option in enumerate(velper_selector_dropdown.options) + if "testuser2" in option.text + ), + None, + ) + velper_selector_dropdown.select_by_index(tu2_index) velp_to_use = self.find_element_avoid_staleness( # Original: "velp-window:nth-child(2) .velp" # Since newVelp-velpWindow is now is outside velp container, diff --git a/timApp/tests/db/test_references.py b/timApp/tests/db/test_references.py index bbf56bcc86..07b0e56729 100644 --- a/timApp/tests/db/test_references.py +++ b/timApp/tests/db/test_references.py @@ -1,8 +1,5 @@ """Unit tests for testing paragraph referencing.""" -import unittest -from typing import Optional - from timApp.document.docparagraph import DocParagraph from timApp.document.document import Document from timApp.document.documentparser import DocumentParser @@ -70,7 +67,6 @@ def setUp(self): self.init_testdb() def init_testdb(self): - db = self.get_db() self.src_doc = self.create_doc().document self.ref_doc = self.create_doc().document @@ -78,7 +74,6 @@ def init_testdb(self): self.assertEqual( self.src_par.get_id(), self.src_doc.get_paragraphs()[0].get_id() ) - return db def test_simpleref(self): ref_par = add_ref_paragraph(self.ref_doc, self.src_par) diff --git a/timApp/tests/unit/test_translator_generic.py b/timApp/tests/db/test_translator_generic.py similarity index 100% rename from timApp/tests/unit/test_translator_generic.py rename to timApp/tests/db/test_translator_generic.py diff --git a/timApp/tests/db/timdbtest.py b/timApp/tests/db/timdbtest.py index f2d6b82685..5324baf508 100644 --- a/timApp/tests/db/timdbtest.py +++ b/timApp/tests/db/timdbtest.py @@ -16,7 +16,6 @@ from timApp.messaging.messagelist.listinfo import Channel from timApp.tim_app import app from timApp.timdb.sqa import db -from timApp.timdb.timdb import TimDb from timApp.user.user import User from timApp.user.usercontact import ContactOrigin from timApp.user.usergroup import UserGroup @@ -30,9 +29,6 @@ class TimDbTest(unittest.TestCase): i = 0 create_docs = False - def get_db(self): - return self.db - @classmethod def setUpClass(cls): if cls.test_files_path.exists(): @@ -52,25 +48,34 @@ def setUpClass(cls): "See https://tim.jyu.fi/view/tim/TIMin-kehitys/PyCharm#testauskonfiguraation-luominen" ) # The following throws if the testing database has not been created yet; we can safely ignore it - try: - db.drop_all() - except sqlalchemy.exc.OperationalError: - pass - except sqlalchemy.exc.InternalError: - # An internal error can happen when switching Git branches that have different database structure. - # In that case, we can just drop the whole test database. - db.session.rollback() - drop_database(app.config["SQLALCHEMY_DATABASE_URI"]) + with app.app_context(): + try: + db.drop_all() + except sqlalchemy.exc.OperationalError: + pass + except sqlalchemy.exc.InternalError: + # An internal error can happen when switching Git branches that have different database structure. + # In that case, we can just drop the whole test database. + db.session.rollback() + drop_database(app.config["SQLALCHEMY_DATABASE_URI"]) timApp.timdb.init.initialize_database(create_docs=cls.create_docs) - def setUp(self): + def check_skip_tests(self): if running_in_ci() and remove_prefix(self.id(), "timApp.") in CI_SKIP_TESTS: self.skipTest("This test is skipped in CI") - self.db = TimDb(files_root_path=self.test_files_path) + + def setUp(self): + self.check_skip_tests() + self.ctx = app.app_context() + self.ctx.__enter__() def tearDown(self): + self.ctx.__exit__(None, None, None) close_all_sessions() - self.db.close() + + def commit_db(self): + db.session.commit() + db.session.expire_all() def create_doc( self, from_file=None, initial_par: str | list[str] = None, settings=None diff --git a/timApp/tests/server/test_access_lock.py b/timApp/tests/server/test_access_lock.py index 09543aa81b..50c36096b2 100644 --- a/timApp/tests/server/test_access_lock.py +++ b/timApp/tests/server/test_access_lock.py @@ -1,4 +1,5 @@ from timApp.auth.accesstype import AccessType +from timApp.item.block import Block from timApp.markdown.markdownconverter import md_to_html from timApp.tests.server.timroutetest import TimRouteTest from timApp.timdb.sqa import db @@ -13,10 +14,6 @@ class AccessLockTest(TimRouteTest): """Tests for access locking and unlocking""" - def tearDown(self): - with self.client.session_transaction() as s: - s.clear() - def test_access_lock_document_redirect(self): """Test that document viewing is restricted when access level is locked.""" @@ -148,10 +145,6 @@ def test_access_lock_tasks(self): class ActiveGroupLockTest(TimRouteTest): """Tests for active group locking and unlocking""" - def tearDown(self): - with self.client.session_transaction() as s: - s.clear() - def test_active_group_lock_visibility(self): self.login_test1() @@ -161,6 +154,8 @@ def test_active_group_lock_visibility(self): self.test_user_3.add_to_group(ug, None) self.test_user_1.remove_access(admin_doc.block.id, "owner") db.session.commit() + ug_id = ug.id + admin_block_id = admin_doc.block.id d = self.create_doc( initial_par=""" @@ -189,7 +184,7 @@ def test_active_group_lock_visibility(self): self.json_post( "/access/groups/lock", { - "group_ids": [ug.id], + "group_ids": [ug_id], }, expect_status=403, ) @@ -203,7 +198,7 @@ def test_active_group_lock_visibility(self): "/access/groups/lock", { "group_ids": [ - ug.id, + ug_id, get_anonymous_group_id(), get_logged_in_group_id(), ], @@ -219,17 +214,20 @@ def test_active_group_lock_visibility(self): ) # Case 3: User can lock active group to testgroup1 (testuser1 has edit access) + # FIXME: SQLAlchemy dynamic ugm: UserGroupMember = self.test_user_1.memberships_dyn.filter( - UserGroupMember.usergroup_id == ug.id + UserGroupMember.usergroup_id == ug_id ).first() ugm.set_expired() - self.test_user_1.grant_access(admin_doc.block, AccessType.edit) + self.test_user_1.grant_access( + db.session.get(Block, admin_block_id), AccessType.edit + ) db.session.commit() self.json_post( "/access/groups/lock", { "group_ids": [ - ug.id, + ug_id, get_anonymous_group_id(), get_logged_in_group_id(), ], @@ -250,7 +248,7 @@ def test_active_group_lock_visibility(self): { "group_ids": [ self.test_user_2.get_personal_group().id, - ug.id, + ug_id, get_anonymous_group_id(), get_logged_in_group_id(), ], @@ -265,7 +263,7 @@ def test_active_group_lock_visibility(self): { "group_ids": [ self.test_user_2.get_personal_group().id, - ug.id, + ug_id, get_anonymous_group_id(), get_logged_in_group_id(), ], diff --git a/timApp/tests/server/test_authors.py b/timApp/tests/server/test_authors.py index 5ebeaa7949..ea7c2d067e 100644 --- a/timApp/tests/server/test_authors.py +++ b/timApp/tests/server/test_authors.py @@ -12,6 +12,7 @@ def test_authors(self): self.test_user_2.grant_access(d, AccessType.edit) self.test_user_3.grant_access(d, AccessType.edit) db.session.commit() + # db.session.expire_all() self.new_par(d.document, "par 1") self.new_par(d.document, "par 2") self.new_par(d.document, "par 3") @@ -20,13 +21,13 @@ def test_authors(self): username_selector = ".authorinfo .username" authors = get_content(self.get(url, as_tree=True), username_selector) self.assertEqual( - authors, ["Logged-in users", "user 1 Test", "user 1 Test", "user 1 Test"] + authors, ["user 1 Test", "user 1 Test", "user 1 Test", "user 1 Test"] ) self.post_par(d.document, "edit", pars[1].get_id()) authors = get_content(self.get(url, as_tree=True), username_selector) self.assertEqual( authors, - ["Logged-in users", "user 1 Test (2 edits)", "user 1 Test", "user 1 Test"], + ["user 1 Test", "user 1 Test (2 edits)", "user 1 Test", "user 1 Test"], ) self.login_test2() self.post_par(d.document, "edit2", pars[1].get_id()) @@ -34,7 +35,7 @@ def test_authors(self): self.assertEqual( authors, [ - "Logged-in users", + "user 1 Test", "user 2 Test; user 1 Test (2 edits)", "user 1 Test", "user 1 Test", @@ -46,7 +47,7 @@ def test_authors(self): self.assertEqual( authors, [ - "Logged-in users", + "user 1 Test", "user 2 Test (2 edits); user 1 Test (2 edits)", "user 1 Test", "user 2 Test; user 1 Test", diff --git a/timApp/tests/server/test_bookmarks.py b/timApp/tests/server/test_bookmarks.py index 20322b93ed..ff7e24faa0 100644 --- a/timApp/tests/server/test_bookmarks.py +++ b/timApp/tests/server/test_bookmarks.py @@ -1,5 +1,7 @@ import re +from flask import g + from timApp.bookmark.bookmarks import Bookmarks from timApp.document.docentry import DocEntry from timApp.document.docinfo import DocInfo @@ -15,6 +17,8 @@ class BookmarkTestBase(TimRouteTest): def get_bookmarks(self, expect_status=200): bms = self.get("/bookmarks/get", expect_status=expect_status) + # Pop the user from the active globals because the above GET will expire it (since session is expired) + g.pop("user", None) return bms @@ -255,7 +259,6 @@ def test_bookmark_migration_to_db(self): class BookmarkTest2(BookmarkTestBase): def test_automatic_course_bookmark_update(self): self.login_test1() - self.get("/") d = self.create_doc() d.block.tags.append(Tag(name="TIEP111", type=TagType.CourseCode)) d.block.tags.append(Tag(name="group:ohj1opiskelijat", type=TagType.Regular)) @@ -264,7 +267,6 @@ def test_automatic_course_bookmark_update(self): d2.block.tags.append(Tag(name="TIEP112", type=TagType.CourseCode)) d2.block.tags.append(Tag(name="group:ohj2opiskelijat", type=TagType.Regular)) db.session.commit() - self.get("/") ug = UserGroup(name="ohj1opiskelijat", display_name="asd asd") tu1 = self.test_user_1 tu1.groups.append(ug) diff --git a/timApp/tests/server/test_broken_db.py b/timApp/tests/server/test_broken_db.py index fce12a442b..86bf888c8e 100644 --- a/timApp/tests/server/test_broken_db.py +++ b/timApp/tests/server/test_broken_db.py @@ -4,11 +4,13 @@ class BrokenDbTest(TimRouteTest): def test_broken_db(self): - db.drop_all() - db.create_all() - # Expire all because otherwise User.query.get(0) would still return anonymous user. db.session.expire_all() + # Also close any pending sessions since drop_all might wait for active sessions to close. + db.session.remove() + + db.drop_all() + db.create_all() with self.assertRaises(Exception) as e: self.get("/") diff --git a/timApp/tests/server/test_caching.py b/timApp/tests/server/test_caching.py index e2a58c33c0..947f3bcaff 100644 --- a/timApp/tests/server/test_caching.py +++ b/timApp/tests/server/test_caching.py @@ -8,7 +8,6 @@ from timApp.item import routes from timApp.item.routes import render_doc_view from timApp.tests.server.timroutetest import TimRouteTest, get_note_id_from_json -from timApp.timdb.sqa import db from timApp.user.usergroup import UserGroup from timApp.user.userutils import grant_access @@ -19,7 +18,7 @@ def test_cache(self): d = self.create_doc(initial_par="#- {plugin=textfield #t}") self.test_user_2.grant_access(d, AccessType.view) self.test_user_3.grant_access(d, AccessType.view) - db.session.commit() + self.commit_db() clear_doc_cache(d, None) self.login_test3() self.check_not_cached(d) @@ -96,7 +95,7 @@ def test_cache_pregenerate(self): self.login_test1() d = self.create_doc(initial_par="test") self.test_user_2.grant_access(d, AccessType.view) - db.session.commit() + self.commit_db() clear_doc_cache(d, None) self.get( f"/generateCache/{d.path}", @@ -137,9 +136,8 @@ def test_cache_pregenerate(self): ug = UserGroup.create("testgroup1") self.test_user_3.add_to_group(ug, added_by=None) grant_access(ug, d, AccessType.view) - db.session.commit() + self.commit_db() d = DocEntry.find_by_id(d.id) - db.session.refresh(d) self.get( f"/generateCache/{d.path}", expect_status=403, @@ -160,7 +158,7 @@ def test_cache_generate_exam_mode(self): self.get(d.url, query_string={"nocache": True}) self.test_user_2.grant_access(d, AccessType.view) - db.session.commit() + self.commit_db() self.get( f"/generateCache/{d.path}", ) diff --git a/timApp/tests/server/test_calendar.py b/timApp/tests/server/test_calendar.py index a6ffc6f24a..f62c57c029 100644 --- a/timApp/tests/server/test_calendar.py +++ b/timApp/tests/server/test_calendar.py @@ -148,7 +148,7 @@ def test_event_message_send(self): self.test_user_2.add_to_group(ug, None) ug.admin_doc = d.block self.test_user_1.grant_access(d.block, AccessType.manage) - db.session.commit() + self.commit_db() self.login_test1() event = self.post_event(self.test_user_1, "event_message_send_test1") diff --git a/timApp/tests/server/test_cbcountfield.py b/timApp/tests/server/test_cbcountfield.py index 0424f8fd11..5b9e2abb49 100644 --- a/timApp/tests/server/test_cbcountfield.py +++ b/timApp/tests/server/test_cbcountfield.py @@ -1,10 +1,9 @@ """Server tests for cbcountfield.""" from timApp.auth.accesstype import AccessType -from timApp.tests.browser.browsertest import BrowserTest -from timApp.timdb.sqa import db +from timApp.tests.server.timroutetest import TimRouteTest -class CbCountFieldTest(BrowserTest): +class CbCountFieldTest(TimRouteTest): def expect_count(self, r, count): self.assertEqual(count, r["web"]["count"]) @@ -17,8 +16,7 @@ def test_cbcountfield(self): ) self.test_user_2.grant_access(d, AccessType.view) self.test_user_3.grant_access(d, AccessType.view) - db.session.commit() - db.session.refresh(d) + self.commit_db() r = self.post_answer( "cbcountfield", f"{d.id}.t", @@ -63,20 +61,21 @@ def test_cbcountfield(self): ) self.expect_count(r, 1) self.login_test3() - r = self.get(d.url, as_tree=True) - par_id = d.document.get_paragraphs()[0].get_id() - self.assert_plugin_json( - r.cssselect(".parContent cbcountfield-runner")[0], - self.create_plugin_json( - d, - "t", - state=None, - info=None, - par_id=par_id, - toplevel={"count": 1}, - markup={"autoUpdateTables": True}, - ), - ) + with self.internal_container_ctx(): + r = self.get(d.url, as_tree=True) + par_id = d.document.get_paragraphs()[0].get_id() + self.assert_plugin_json( + r.cssselect(".parContent cbcountfield-runner")[0], + self.create_plugin_json( + d, + "t", + state=None, + info=None, + par_id=par_id, + toplevel={"count": 1}, + markup={"autoUpdateTables": True}, + ), + ) def test_cbcountfield_grouplogin(self): self.login_test1() diff --git a/timApp/tests/server/test_folders.py b/timApp/tests/server/test_folders.py index fe29136ca6..0499dcb68e 100644 --- a/timApp/tests/server/test_folders.py +++ b/timApp/tests/server/test_folders.py @@ -3,7 +3,6 @@ from sqlalchemy import event from timApp.auth.accesstype import AccessType -from timApp.auth.auth_models import BlockAccess from timApp.document.docentry import DocEntry from timApp.folder.folder import Folder from timApp.item.block import BlockType @@ -174,7 +173,7 @@ def test_folder_view_perf(self): self.create_doc(self.get_personal_item_path("perf/x")) d = self.create_doc(self.get_personal_item_path("perf/y")) self.get(d.url) - eng = db.get_engine() + eng = db.engine stmts = 0 db.session.expunge_all() @@ -187,11 +186,30 @@ def before_cursor_execute( nonlocal stmts stmts += 1 + item_path = self.get_personal_item_path("perf") event.listen(eng, "before_cursor_execute", before_cursor_execute) - self.get("/view/" + self.get_personal_item_path("perf")) + self.get(f"/view/{item_path}") event.remove(eng, "before_cursor_execute", before_cursor_execute) - self.assertEqual(stmts, 11) + # NOTE: In general, the number of statements should be kept as low as possible for fast performance. + # Usually the main reason for statement number increase is the use of eager relationship loading. + # However, one should also balance the amount of data loaded. + # For example, selectinload() will always increase statement count, but will likely simplify queries + # and will reduce the amount of data the database has to return. + # + # In general, if this test fails, then + # 1. Place a breakpoint into before_cursor_execute() + # 2. Debug the test + # 3. Step through each statement and check the call stack to see where the statement is originating from + # 4. Check the queries that generate the statements and inspect any relationships. + # - Check if any relationships are loaded lazily (e.g. lazy="select"). Check if they can be loaded eagerly. + # - By default, use selectinload() + # - Any relationships that return only one object (one-to-one, many-to-one) may use joinedload() + # to reduce the number of statements + # - If unsure, try joinedload() and see if the test fails. If so, use selectinload(). + # 5. If all optimizations are done, update the expected statement count below. + + self.assertEqual(stmts, 13) class FolderCopyTest(TimRouteTest): @@ -328,48 +346,32 @@ def test_folder_copy(self): f2c = Folder.find_by_path(self.get_personal_item_path("b/f2")) t1g = self.test_user_1.get_personal_group() self.assertEqual( - [ - BlockAccess( - block_id=f1c.id, usergroup_id=t1g.id, type=AccessType.owner.value - ), - BlockAccess( - block_id=f1c.id, usergroup_id=t2g.id, type=AccessType.view.value - ), - ], - list(f1c.block.accesses.values()), + { + (f1c.id, t1g.id, AccessType.owner.value), + (f1c.id, t2g.id, AccessType.view.value), + }, + {(a.block_id, a.usergroup_id, a.type) for a in f1c.block.accesses.values()}, ) self.assertEqual( - [ - BlockAccess( - block_id=f2c.id, usergroup_id=t1g.id, type=AccessType.owner.value - ), - BlockAccess( - block_id=f2c.id, usergroup_id=t2g.id, type=AccessType.edit.value - ), - ], - list(f2c.block.accesses.values()), + { + (f2c.id, t1g.id, AccessType.owner.value), + (f2c.id, t2g.id, AccessType.edit.value), + }, + {(a.block_id, a.usergroup_id, a.type) for a in f2c.block.accesses.values()}, ) self.assertEqual( - [ - BlockAccess( - block_id=d2c.id, usergroup_id=t1g.id, type=AccessType.owner.value - ), - BlockAccess( - block_id=d2c.id, usergroup_id=t2g.id, type=AccessType.teacher.value - ), - ], - list(d2c.block.accesses.values()), + { + (d2c.id, t1g.id, AccessType.owner.value), + (d2c.id, t2g.id, AccessType.teacher.value), + }, + {(a.block_id, a.usergroup_id, a.type) for a in d2c.block.accesses.values()}, ) self.assertEqual( - [ - BlockAccess( - block_id=d2.id, usergroup_id=t1g.id, type=AccessType.owner.value - ), - BlockAccess( - block_id=d2.id, usergroup_id=t2g.id, type=AccessType.teacher.value - ), - ], - list(d2.block.accesses.values()), + { + (d2.id, t1g.id, AccessType.owner.value), + (d2.id, t2g.id, AccessType.teacher.value), + }, + {(a.block_id, a.usergroup_id, a.type) for a in d2.block.accesses.values()}, ) trs = sorted(f1d1c.translations, key=lambda tr: tr.lang_id) self.assertEqual(["", "en", "sv"], [tr.lang_id for tr in trs]) diff --git a/timApp/tests/server/test_groups.py b/timApp/tests/server/test_groups.py index 55f5394ff4..db3fd8a07d 100644 --- a/timApp/tests/server/test_groups.py +++ b/timApp/tests/server/test_groups.py @@ -316,6 +316,7 @@ def test_groups_trim(self): ) def test_invalid_group_setting(self): + self.login_test1() d = self.create_doc(settings={"group": ["a", "b"]}) html = self.get(d.get_url_for_view("teacher")) self.assertIn("The setting 'group' must be a string", html) diff --git a/timApp/tests/server/test_importdata.py b/timApp/tests/server/test_importdata.py index 90327e87a7..013782e7e5 100644 --- a/timApp/tests/server/test_importdata.py +++ b/timApp/tests/server/test_importdata.py @@ -1,11 +1,7 @@ """Server tests for importData plugin.""" import json -from contextlib import contextmanager -import responses -from requests import PreparedRequest - -from timApp.tests.browser.browsertest import BrowserTest +from timApp.tests.server.timroutetest import TimRouteTest from timApp.timdb.sqa import db from timApp.user.personaluniquecode import SchacPersonalUniqueCode, PersonalUniqueCode from timApp.user.user import User @@ -58,18 +54,27 @@ def field_result( } -class ImportDataTestBase(BrowserTest): +class ImportDataTestBase(TimRouteTest): def imp(self, d, data, expect, status: int, task=None, aalto_return=None): if not task: task = "t" - with self.importdata_ctx(aalto_return): - self.post_answer( - "importData", - f"{d.id}.{task}", - data, - expect_content=expect, - expect_status=status, - ) + + def init_mock(m): + m.add( + "GET", + "https://plus.cs.aalto.fi/api/v2/courses/1234/aggregatedata/?format=json", + body=json.dumps(aalto_return), + status=200, + ) + + self.post_answer( + "importData", + f"{d.id}.{task}", + data, + expect_content=expect, + expect_status=status, + init_mock=init_mock, + ) def grant_user_creation_right(self): self.current_user.add_to_group( diff --git a/timApp/tests/server/test_jsrunner.py b/timApp/tests/server/test_jsrunner.py index 3b4a0535cd..5ab5a035e2 100644 --- a/timApp/tests/server/test_jsrunner.py +++ b/timApp/tests/server/test_jsrunner.py @@ -1,10 +1,9 @@ """Server tests for jsrunner plugin.""" import json from datetime import datetime, timezone -from select import select import requests -from sqlalchemy import func +from sqlalchemy import func, select from timApp.answer.answer import Answer from timApp.auth.accesstype import AccessType @@ -453,7 +452,7 @@ def test_rights(self): """ ) self.test_user_2.grant_access(d, AccessType.view) - db.session.commit() + self.commit_db() self.login_test2() d2 = self.create_jsrun( f""" @@ -1151,7 +1150,7 @@ def test_global_field(self): db.session.commit() total_answer_count_before = db.session.scalar(select(func.count(Answer.id))) self.do_jsrun(d) - total_answer_count_after = db.session.scalar(select(func.count(Answer))) + total_answer_count_after = db.session.scalar(select(func.count(Answer.id))) self.assertEqual(1, total_answer_count_after - total_answer_count_before) self.verify_answer_content( f"{d.id}.GLO_a", "c", "ab", self.test_user_1, expected_count=2 diff --git a/timApp/tests/server/test_math.py b/timApp/tests/server/test_math.py index da14fe0607..b31da11b85 100644 --- a/timApp/tests/server/test_math.py +++ b/timApp/tests/server/test_math.py @@ -33,6 +33,9 @@ class MathTest(TimRouteTest): def test_svg_math(self): + self.impl_test_svg_math() + + def impl_test_svg_math(self): self.login_test1() d = self.create_doc(initial_par="$a+b$") d.document.set_settings({"math_type": "svg"}) @@ -64,7 +67,7 @@ def test_mathjax_math(self): ) def test_mathtype_change(self): - d = self.test_svg_math() + d = self.impl_test_svg_math() d.document.set_settings({"math_type": "mathjax"}) self.assert_same_html( self.get(d.url, as_tree=True).cssselect(".parContent")[1], mathjax_html diff --git a/timApp/tests/server/test_minutes.py b/timApp/tests/server/test_minutes.py index 81e30dee54..20cd86dc1c 100644 --- a/timApp/tests/server/test_minutes.py +++ b/timApp/tests/server/test_minutes.py @@ -6,7 +6,6 @@ PRINT_FOLDER_NAME, PREAMBLE_FOLDER_NAME, ) -from timApp.tests.browser.browsertest import BrowserTest from timApp.tests.server.timroutetest import TimRouteTest from timApp.timdb.sqa import db from timApp.user.usergroup import UserGroup @@ -44,10 +43,12 @@ def test_minutes_creation(self): self.get(f"/images/{image_path}", expect_status=403) -# BrowserTest is required because test needs in-process plugin (timTable) and -# TimRouteTest's testclient is not multithreaded. -class MinutesHandling(BrowserTest): +class MinutesHandling(TimRouteTest): def test_minute_extracts(self): + with self.internal_container_ctx(): + self.impl_test_minute_extracts() + + def impl_test_minute_extracts(self): # Tests creation of extracts from a full minutes document self.login_test1() ug1 = UserGroup.create("ittdk18") diff --git a/timApp/tests/server/test_peer_review.py b/timApp/tests/server/test_peer_review.py index f1a48d2c46..44799b7a7b 100644 --- a/timApp/tests/server/test_peer_review.py +++ b/timApp/tests/server/test_peer_review.py @@ -116,7 +116,7 @@ def check_peerreview_rows_t(): expect_status=400, expect_content={"error": "Requested block is inside an area"}, ) - self.assertEqual(0, len(rq.all())) + self.assertEqual(0, len(db.session.execute(rq).all())) tu1_ans = self.add_answer(d, "ta1", "tu1", user=self.test_user_1) tu3_ans = self.add_answer(d, "ta2", "tu2", user=self.test_user_3) db.session.commit() @@ -125,7 +125,7 @@ def check_peerreview_rows_t(): expect_status=400, expect_content={"error": "Area revs not found"}, ) - self.assertEqual(0, len(rq.all())) + self.assertEqual(0, len(db.session.execute(rq).all())) d.document.add_setting("group", "testusers1") self.get(f"{url}?area=rev") prs: list[PeerReview] = ( @@ -176,10 +176,10 @@ def check_area_prs(): self.assertIn( "Not enough users to form pairs (1 but at least 2 users needed)", r ) - self.assertEqual(0, len(rq.all())) + self.assertEqual(0, len(db.session.execute(rq).all())) d.document.add_setting("peer_review_allow_invalid", True) r = self.get(f"{url}?b={b}&size=1") self.assertNotIn( "Not enough users to form pairs (1 but at least 2 users needed)", r ) - self.assertEqual(2, len(rq.all())) + self.assertEqual(2, len(db.session.execute(rq).all())) diff --git a/timApp/tests/server/test_permissions.py b/timApp/tests/server/test_permissions.py index d53fc2d4c7..6b650e058e 100644 --- a/timApp/tests/server/test_permissions.py +++ b/timApp/tests/server/test_permissions.py @@ -1,4 +1,3 @@ -import time from datetime import timedelta from timApp.auth.accesstype import AccessType @@ -645,6 +644,7 @@ def test_auto_confirm_translation(self): self.login_test1() d = self.create_doc() tr = self.create_translation(d) + tr_url = tr.url self.test_user_2.grant_access( access_type=AccessType.view, accessible_to=get_current_time() - timedelta(days=1), @@ -686,7 +686,7 @@ def test_auto_confirm_translation(self): self.get(d2tr.url, expect_status=403) self.get(d2tr.url, expect_status=403) - r = self.get(tr.url, expect_status=403) + r = self.get(tr_url, expect_status=403) self.assertIn("My custom message", r) self.assertIn("Go to the next document", r) self.get(d2tr.url) @@ -756,6 +756,7 @@ def test_inherit_rights_from_folder(self): ) uf = self.upload_file(d, b"test", "test.txt") tr = self.create_translation(d) + tr_url = tr.url f = d.parent self.test_user_2.grant_access(f, AccessType.view) db.session.commit() @@ -765,7 +766,7 @@ def test_inherit_rights_from_folder(self): self.get(f'/files/{uf["file"]}', expect_status=403) with self.temp_config({"INHERIT_FOLDER_RIGHTS_DOCS": {d.path}}): self.get(d.url) - self.get(tr.url) + self.get(tr_url) self.get(f'/files/{uf["file"]}') self.mark_as_read(d, d.document.get_paragraphs()[0].get_id()) self.post_answer("textfield", f"{d.id}.t", user_input={"c": "x"}) @@ -984,6 +985,9 @@ def test_confirm_translation_permissions(self): expect_status=200, ) + t1 = DocEntry.find_by_id(t1.id) + t2 = DocEntry.find_by_id(t2.id) + self.assertFalse(self.test_user_2.has_view_access(d)) self.assertFalse(self.test_user_3.has_view_access(d)) self.assertFalse(self.test_user_2.has_view_access(t1)) diff --git a/timApp/tests/db/test_personal_folder.py b/timApp/tests/server/test_personal_folder.py similarity index 100% rename from timApp/tests/db/test_personal_folder.py rename to timApp/tests/server/test_personal_folder.py diff --git a/timApp/tests/server/test_plugins.py b/timApp/tests/server/test_plugins.py index 809cb41200..987e2dd785 100644 --- a/timApp/tests/server/test_plugins.py +++ b/timApp/tests/server/test_plugins.py @@ -555,10 +555,14 @@ def test_broken_upload(self): self.do_plugin_upload(d, "test", "test.txt", f"{d.id}.testupload", "testupload") self.get(f"/uploads/{d.id}/testupload/testuser1/1/test.txt") a = ( - select(Answer) - .filter_by(task_id=f"{d.id}.testupload") - .join(AnswerUpload) - .with_only_columns(AnswerUpload) + db.session.execute( + select(Answer) + .filter_by(task_id=f"{d.id}.testupload") + .join(AnswerUpload) + .with_only_columns(AnswerUpload) + .limit(1) + ) + .scalars() .first() ) @@ -1006,7 +1010,6 @@ def get_pts(rule): self.login_test1() doc = self.create_doc(from_file=static_tim_doc("mmcq_example.md")) d = doc.document - timdb = self.get_db() self.test_user_2.grant_access(doc, AccessType.view) db.session.commit() task_ids = [ @@ -1508,7 +1511,7 @@ def test_answer_rename(self): self.assertEqual( 2, db.session.scalar( - select(func.count(Answer)).filter_by(task_id=f"{d.id}.t_new") + select(func.count(Answer.id)).filter_by(task_id=f"{d.id}.t_new") ), ) self.post_answer(p2.type, p2.task_id.doc_task, [True, True, False]) @@ -1955,9 +1958,10 @@ def test_translation_plugin_state(self): """ ) tr = self.create_translation(d) + tr_url = tr.url s = {"userword": "test"} self.post_answer("pali", f"{d.id}.t", user_input=s) - r = self.get(tr.url, as_tree=True) + r = self.get(tr_url, as_tree=True) self.assert_plugin_json( r.cssselect(".parContent pali-runner")[0], self.create_plugin_json( diff --git a/timApp/tests/server/test_plugins_preamble.py b/timApp/tests/server/test_plugins_preamble.py index ed1161181f..2d294c4b43 100644 --- a/timApp/tests/server/test_plugins_preamble.py +++ b/timApp/tests/server/test_plugins_preamble.py @@ -1,6 +1,7 @@ from lxml import html from timApp.answer.answer import Answer +from timApp.document.docentry import DocEntry from timApp.document.docinfo import DocInfo from timApp.document.docparagraph import DocParagraph from timApp.document.viewcontext import default_view_ctx @@ -60,6 +61,9 @@ def run_plugin_in_preamble(self, doc_path: str, create_preamble_translation=True a: Answer = db.session.get(Answer, resp["savedNew"]) self.assertEqual(1 if create_preamble_translation else 0, a.points) self.assertEqual(f"{d.id}.t", a.task_id) + # Reattach the documents to the session + db.session.add(tr) + d = DocEntry.find_by_id(d.id) self.check_plugin_ref_correct( tr, d, p.document.get_paragraphs()[0], preamble_doc=tr_p ) @@ -177,8 +181,11 @@ def check_plugin_ref_correct( .getparent() .getparent() ) + doc_to_check = DocEntry.find_by_id(doc_to_check.id) + expected_doc = DocEntry.find_by_id(expected_doc.id) # print(html.tostring(par, pretty_print=True).decode()) if preamble_doc: + preamble_doc = DocEntry.find_by_id(preamble_doc.id) self.assertEqual(preamble_doc.path, par.attrib["data-from-preamble"]) else: self.assertIsNone(par.attrib.get("data-from-preamble")) diff --git a/timApp/tests/server/test_preamble.py b/timApp/tests/server/test_preamble.py index e7eacb4887..a6132bc897 100644 --- a/timApp/tests/server/test_preamble.py +++ b/timApp/tests/server/test_preamble.py @@ -301,6 +301,7 @@ def test_multiple_preamble(self): p = p2ctr.document.get_paragraphs()[0] p.set_markdown("p2ctr") p.save() + db.session.add(dt) e = self.get(dt.url, as_tree=True) self.assert_content(e, ["p1c", "p1", "p2ctr", "p2", "p3c", "p3", ""]) d.document.set_settings({"preamble": "chat, preamblez"}) diff --git a/timApp/tests/server/test_printing.py b/timApp/tests/server/test_printing.py index ea67ae5f13..00e7c08c60 100644 --- a/timApp/tests/server/test_printing.py +++ b/timApp/tests/server/test_printing.py @@ -128,7 +128,7 @@ def test_print_latex_pdf(self): # Just check the file size for now. pdf_length = len(result) self.assertTrue( - 2809 <= pdf_length <= 2910, msg=f"Unexpected file length: {pdf_length}" + 2809 <= pdf_length <= 2911, msg=f"Unexpected file length: {pdf_length}" ) self.login_test2() self.get(expected_url, expect_status=403) diff --git a/timApp/tests/server/test_question.py b/timApp/tests/server/test_question.py index ce34df237e..e0234c4e27 100644 --- a/timApp/tests/server/test_question.py +++ b/timApp/tests/server/test_question.py @@ -3,55 +3,56 @@ from lxml import html from timApp.auth.accesstype import AccessType -from timApp.tests.browser.browsertest import BrowserTest +from timApp.tests.server.timroutetest import TimRouteTest from timApp.timdb.sqa import db from timApp.util.utils import static_tim_doc -class QuestionTest(BrowserTest): +class QuestionTest(TimRouteTest): def test_question_html(self): - self.login_test1() - d = self.create_doc(from_file=static_tim_doc("questions.md")) - pars = d.document.get_paragraphs() - data = self.get(d.url_relative, as_tree=True) - first_id = pars[0].get_id() - self.assert_plugin_json( - data.cssselect(".parContent tim-qst")[0], - self.create_plugin_json( - d, - "test1", - par_id=first_id, - toplevel={"show_result": False}, - markup={ - "answerFieldType": "radio", - "headers": [], - "isTask": False, - "questionText": "What day is it today?", - "questionTitle": "Today", - "questionType": "radio-vertical", - "rows": ["Monday", "Wednesday", "Friday"], - "timeLimit": 90, - }, - ), - ) + with self.internal_container_ctx(): + self.login_test1() + d = self.create_doc(from_file=static_tim_doc("questions.md")) + pars = d.document.get_paragraphs() + data = self.get(d.url_relative, as_tree=True) + first_id = pars[0].get_id() + self.assert_plugin_json( + data.cssselect(".parContent tim-qst")[0], + self.create_plugin_json( + d, + "test1", + par_id=first_id, + toplevel={"show_result": False}, + markup={ + "answerFieldType": "radio", + "headers": [], + "isTask": False, + "questionText": "What day is it today?", + "questionTitle": "Today", + "questionType": "radio-vertical", + "rows": ["Monday", "Wednesday", "Friday"], + "timeLimit": 90, + }, + ), + ) - second_id = pars[1].get_id() - result = data.cssselect(f"#{second_id} .parContent tim-qst") - self.assertEqual(1, len(result)) + second_id = pars[1].get_id() + result = data.cssselect(f"#{second_id} .parContent tim-qst") + self.assertEqual(1, len(result)) - expected_element = html.fromstring( - f""" -
- - - # - - {'test3'} - -
+ expected_element = html.fromstring( + f""" +
+ + + # + + {'test3'} + +

 json:
   answerFieldType: radio
@@ -65,93 +66,94 @@ def test_question_html(self):
   - Friday
   timeLimit: 90
 points: '2:1'
+
+
+
-
-
-
- """ - ) - self.assert_elements_equal( - expected_element, data.cssselect("#" + pars[2].get_id())[0] - ) + """ + ) + self.assert_elements_equal( + expected_element, data.cssselect("#" + pars[2].get_id())[0] + ) - self.get( - "/getQuestionByParId", - query_string={"doc_id": d.id, "par_id": first_id}, - expect_content={ - "docId": d.id, - "markup": { - "answerFieldType": "radio", - "defaultPoints": 0.5, - "headers": [], - "points": "2:1", - "questionText": "What day is it today?", - "questionTitle": "Today", - "questionType": "radio-vertical", - "rows": ["Monday", "Wednesday", "Friday"], - "timeLimit": 90, + self.get( + "/getQuestionByParId", + query_string={"doc_id": d.id, "par_id": first_id}, + expect_content={ + "docId": d.id, + "markup": { + "answerFieldType": "radio", + "defaultPoints": 0.5, + "headers": [], + "points": "2:1", + "questionText": "What day is it today?", + "questionTitle": "Today", + "questionType": "radio-vertical", + "rows": ["Monday", "Wednesday", "Friday"], + "timeLimit": 90, + }, + "parId": first_id, + "qst": True, + "isPreamble": False, + "taskId": "test1", }, - "parId": first_id, - "qst": True, - "isPreamble": False, - "taskId": "test1", - }, - ) + ) - self.get( - "/getQuestionByParId", - query_string={"doc_id": d.id, "par_id": second_id}, - expect_content={ - "docId": d.id, - "markup": { - "answerFieldType": "radio", - "headers": [], - "points": "2:1", - "questionText": "What day is it today?", - "questionTitle": "Today", - "questionType": "radio-vertical", - "rows": ["Monday", "Wednesday", "Friday"], - "timeLimit": 1, + self.get( + "/getQuestionByParId", + query_string={"doc_id": d.id, "par_id": second_id}, + expect_content={ + "docId": d.id, + "markup": { + "answerFieldType": "radio", + "headers": [], + "points": "2:1", + "questionText": "What day is it today?", + "questionTitle": "Today", + "questionType": "radio-vertical", + "rows": ["Monday", "Wednesday", "Friday"], + "timeLimit": 1, + }, + "parId": second_id, + "qst": False, + "isPreamble": False, + "taskId": "test2", }, - "parId": second_id, - "qst": False, - "isPreamble": False, - "taskId": "test2", - }, - ) + ) - self.get( - "/getQuestionByParId", - query_string={"doc_id": d.id, "par_id": pars[2].get_id()}, - expect_status=400, - expect_content={"error": f"Paragraph is not a plugin: {pars[2].get_id()}"}, - ) + self.get( + "/getQuestionByParId", + query_string={"doc_id": d.id, "par_id": pars[2].get_id()}, + expect_status=400, + expect_content={"error": f"Paragraph is not a plugin: {pars[2].get_id()}"}, + ) - normal_par_id = pars[3].get_id() - self.get( - "/getQuestionByParId", - query_string={"doc_id": d.id, "par_id": normal_par_id}, - expect_status=400, - expect_content={"error": f"Paragraph is not a plugin: {normal_par_id}"}, - ) + normal_par_id = pars[3].get_id() + self.get( + "/getQuestionByParId", + query_string={"doc_id": d.id, "par_id": normal_par_id}, + expect_status=400, + expect_content={"error": f"Paragraph is not a plugin: {normal_par_id}"}, + ) def test_question_invalid_numeric_keys(self): - self.login_test1() - d = self.create_doc( - initial_par=""" + with self.internal_container_ctx(): + self.login_test1() + d = self.create_doc( + initial_par=""" #- {plugin=qst} 1:1| #- {plugin=qst} 1:[] - """ - ) - self.get(d.url) +""" + ) + self.get(d.url) - def test_hidden_points(self): - self.login_test1() - d = self.create_doc( - initial_par=""" + def test_hidden_points(self): + self.login_test1() + d = self.create_doc( + initial_par=""" #- {#t plugin=qst question="false"} answerFieldType: radio answerLimit: 1 @@ -169,58 +171,58 @@ def test_hidden_points(self): - x - def """ - ) - d.document.set_settings({"global_plugin_attrs": {"qst": {"showPoints": False}}}) - self.test_user_2.grant_access(d, AccessType.view) - db.session.commit() - db.session.refresh(d) - r = self.post_answer("qst", f"{d.id}.t", user_input={"answers": [["2"], ["1"]]}) - self.assertEqual( - { - "markup": { - "answerFieldType": "radio", - "answerLimit": 1, - "headers": ["a", "b"], - "matrixType": "radiobutton-horizontal", - "questionText": "test", - "questionTitle": "test", - "questionType": "matrix", - "rows": ["x", "def"], - "showPoints": False, + ) + d.document.set_settings({"global_plugin_attrs": {"qst": {"showPoints": False}}}) + self.test_user_2.grant_access(d, AccessType.view) + db.session.commit() + db.session.refresh(d) + r = self.post_answer("qst", f"{d.id}.t", user_input={"answers": [["2"], ["1"]]}) + self.assertEqual( + { + "markup": { + "answerFieldType": "radio", + "answerLimit": 1, + "headers": ["a", "b"], + "matrixType": "radiobutton-horizontal", + "questionText": "test", + "questionTitle": "test", + "questionType": "matrix", + "rows": ["x", "def"], + "showPoints": False, + }, + "result": "Saved", + "show_result": True, + "state": [["2"], ["1"]], }, - "result": "Saved", - "show_result": True, - "state": [["2"], ["1"]], - }, - r["web"], - ) - self.assertTrue("error" not in r) - answers = self.get_task_answers(f"{d.id}.t", self.current_user) - self.assertEqual(0.5, answers[0]["points"]) - self.login_test2() - r = self.post_answer("qst", f"{d.id}.t", user_input={"answers": [["2"], ["1"]]}) - self.assertEqual( - { - "markup": { - "answerFieldType": "radio", - "answerLimit": 1, - "headers": ["a", "b"], - "matrixType": "radiobutton-horizontal", - "questionText": "test", - "questionTitle": "test", - "questionType": "matrix", - "rows": ["x", "def"], - "showPoints": False, + r["web"], + ) + self.assertTrue("error" not in r) + answers = self.get_task_answers(f"{d.id}.t", self.current_user) + self.assertEqual(0.5, answers[0]["points"]) + self.login_test2() + r = self.post_answer("qst", f"{d.id}.t", user_input={"answers": [["2"], ["1"]]}) + self.assertEqual( + { + "markup": { + "answerFieldType": "radio", + "answerLimit": 1, + "headers": ["a", "b"], + "matrixType": "radiobutton-horizontal", + "questionText": "test", + "questionTitle": "test", + "questionType": "matrix", + "rows": ["x", "def"], + "showPoints": False, + }, + "result": "Saved", + "show_result": True, + "state": [["2"], ["1"]], }, - "result": "Saved", - "show_result": True, - "state": [["2"], ["1"]], - }, - r["web"], - ) - self.assertTrue("error" not in r) - answers = self.get_task_answers(f"{d.id}.t", self.current_user) - self.assertEqual(None, answers[0]["points"]) # Should be hidden. + r["web"], + ) + self.assertTrue("error" not in r) + answers = self.get_task_answers(f"{d.id}.t", self.current_user) + self.assertEqual(None, answers[0]["points"]) # Should be hidden. def test_question_shuffle(self): """ @@ -285,6 +287,7 @@ def test_question_shuffle(self): """ ) db.session.commit() + db.session.add(d) db.session.refresh(d) self.login_test3() self.post_answer( diff --git a/timApp/tests/server/test_scheduled_functions.py b/timApp/tests/server/test_scheduled_functions.py index f82eecbd22..f8edd1dcf0 100644 --- a/timApp/tests/server/test_scheduled_functions.py +++ b/timApp/tests/server/test_scheduled_functions.py @@ -1,3 +1,4 @@ +import json from contextlib import contextmanager from datetime import timedelta @@ -104,6 +105,7 @@ def no_request_context(self): with app.app_context(): yield self.client.__enter__() + self.get("/") def test_scheduled_function(self): self.login_test1() @@ -301,8 +303,11 @@ def test_import_function_with_create_users(self): u.groups.append(UserGroup.get_user_creator_group()) db.session.commit() with self.no_request_context(): - with self.importdata_ctx( - [ + with self.internal_container_ctx() as m: + m.add( + "GET", + "https://plus.cs.aalto.fi/api/v2/courses/1234/aggregatedata/?format=json", + body=json.dumps([ { "UserID": 123, "StudentID": "12345X", @@ -315,8 +320,9 @@ def test_import_function_with_create_users(self): "2 Total": 0, "2 Ratio": 0.0, } - ] - ): + ]), + status=200, + ) do_run_user_function( self.test_user_2.id, f"{d.id}.import", {"token": "abc"} ) diff --git a/timApp/tests/server/test_showfile.py b/timApp/tests/server/test_showfile.py index 3d35e5d3d2..5ec8c148cb 100644 --- a/timApp/tests/server/test_showfile.py +++ b/timApp/tests/server/test_showfile.py @@ -1,17 +1,41 @@ -from timApp.tests.browser.browsertest import BrowserTest +from timApp.tests.server.timroutetest import TimRouteTest -class ShowfileTest(BrowserTest): - def test_no_local_file_access(self): - self.login_test1() - d = self.create_doc( - initial_par=f""" +class ShowfileTest(TimRouteTest): + def test_show_file_basic(self): + from flask import Flask + from werkzeug.serving import make_server + import threading + + app = Flask(__name__) + + @app.get("/ping") + def ping(): + return {"status": "ok"} + + server = make_server("", 8080, app) + + def run_server(): + server.serve_forever() + + t = threading.Thread(target=run_server) + t.start() + + try: + self.login_test1() + d = self.create_doc( + initial_par=f""" #- {{#t plugin=showCode}} -file: {self.get_browser_url()}/ping - """ - ) - self.assert_content(self.get(d.url, as_tree=True), ['{"status":"ok"}\nping']) +file: http://tests:8080/ping +""" + ) + self.assert_content(self.get(d.url, as_tree=True), ['{"status":"ok"}\nping']) + finally: + server.shutdown() + t.join() + def test_no_local_file_access(self): + self.login_test1() d = self.create_doc( initial_par=""" #- {#t plugin=showCode} @@ -22,3 +46,5 @@ def test_no_local_file_access(self): self.get(d.url, as_tree=True), ["URL scheme must be http or https, got 'file'something"], ) + + diff --git a/timApp/tests/server/test_signup.py b/timApp/tests/server/test_signup.py index 7c6f505643..9e54becd19 100644 --- a/timApp/tests/server/test_signup.py +++ b/timApp/tests/server/test_signup.py @@ -143,7 +143,7 @@ def test_signup_case_insensitive(self): self.json_post("/emailSignup", {"email": email}) self.assertEqual( db.session.execute(select(NewUser.email)).scalars().all(), - [("someonecase@example.com",)], + ["someonecase@example.com"], ) self.json_post( "/checkTempPass", @@ -169,7 +169,7 @@ def test_signup_whitespace(self): self.json_post("/emailSignup", {"email": email}) self.assertEqual( db.session.execute(select(NewUser.email)).scalars().all(), - [("whitespace@example.com",)], + ["whitespace@example.com"], ) self.json_post( "/checkTempPass", @@ -218,9 +218,9 @@ def test_login_case_insensitive(self): def test_signup(self): email = "testingsignup@example.com" self.json_post("/emailSignup", {"email": email}) - self.assertEqual(db.session.execute(select(NewUser.email)).scalars().all(), [(email,)]) + self.assertEqual(db.session.execute(select(NewUser.email)).scalars().all(), [email]) self.json_post("/emailSignup", {"email": email}) - self.assertEqual(db.session.execute(select(NewUser.email)).scalars().all(), [(email,)]) + self.assertEqual(db.session.execute(select(NewUser.email)).scalars().all(), [email]) self.json_post( "/emailSignupFinish", { diff --git a/timApp/tests/server/test_translation.py b/timApp/tests/server/test_translation.py index a6061a979c..89ba77dbc0 100644 --- a/timApp/tests/server/test_translation.py +++ b/timApp/tests/server/test_translation.py @@ -1,3 +1,4 @@ +from typing import Type from unittest.mock import patch, Mock from sqlalchemy import select @@ -17,7 +18,9 @@ from timApp.document.translation.translation import Translation from timApp.document.translation.translator import Usage, TranslateBlock from timApp.document.yamlblock import YamlBlock +from timApp.tests.db.timdbtest import TimDbTest from timApp.tests.server.timroutetest import TimRouteTest +from timApp.tim_app import app from timApp.timdb.sqa import db from timApp.util.utils import static_tim_doc @@ -27,7 +30,16 @@ """ -class TimTranslationTest(TimRouteTest): +def setup_translation_test(cls: Type) -> None: + with app.app_context(): + db.session.add(ReversingTranslationService()) + db.session.add(QuotaLimitedTestTranslator()) + cls.reverselang = Language(**REVERSE_LANG) + db.session.add(cls.reverselang) + db.session.commit() + + +class TimTranslationTest(TimDbTest): """Test class containing the reversing translation service and its preferred target language. """ @@ -35,11 +47,22 @@ class TimTranslationTest(TimRouteTest): @classmethod def setUpClass(cls): super().setUpClass() - db.session.add(ReversingTranslationService()) - db.session.add(QuotaLimitedTestTranslator()) - cls.reverselang = Language(**REVERSE_LANG) - db.session.add(cls.reverselang) - db.session.commit() + setup_translation_test(cls) + + @property + def reverselang(self) -> Language: + return db.session.get(Language, REVERSE_LANG["lang_code"]) + + +class TimTranslationRouteTest(TimRouteTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + setup_translation_test(cls) + + @property + def reverselang(self) -> Language: + return db.session.get(Language, REVERSE_LANG["lang_code"]) class QuotaLimitedTestTranslator(ReversingTranslationService): @@ -84,7 +107,7 @@ def usage(self) -> Usage: __mapper_args__ = {"polymorphic_identity": "QuotaLimited"} -class TranslationTest(TimTranslationTest): +class TranslationTest(TimTranslationRouteTest): def get_deepl_service(self) -> DeeplTranslationService: return ( db.session.execute(select(DeeplTranslationService).limit(1)) diff --git a/timApp/tests/server/test_upload.py b/timApp/tests/server/test_upload.py index 41896d52da..2e891fcfca 100644 --- a/timApp/tests/server/test_upload.py +++ b/timApp/tests/server/test_upload.py @@ -224,4 +224,4 @@ def test_upload_mimetype(self): r = self.upload_file( d, b"""""", "test.svg" ) - self.check_mime(r["file"], "image/svg+xml") + self.check_mime(r["image"], "image/svg+xml") diff --git a/timApp/tests/server/test_velp.py b/timApp/tests/server/test_velp.py index 1d7b6a88e0..6c11572b7c 100644 --- a/timApp/tests/server/test_velp.py +++ b/timApp/tests/server/test_velp.py @@ -748,15 +748,14 @@ def test_deleted_velp_group_permissions(self): # test user 2 should not have access anymore self.get(deleted.url, expect_status=403) # admin should still be able to access - test_user_2 = get_current_user_object() - self.make_admin(test_user_2) + self.make_admin(self.test_user_2) self.get(deleted.url, expect_status=200) # remove testuser2 from admin group to prevent it from affecting other tests self.json_post( f"/groups/removemember/{UserGroup.get_admin_group().name}", - {"names": [test_user_2.name]}, + {"names": [self.test_user_2.name]}, expect_content={ - "removed": [test_user_2.name], + "removed": [self.test_user_2.name], "does_not_belong": [], "not_exist": [], }, @@ -1007,6 +1006,7 @@ def test_velp_group_permissions_path(self): {"name": "folder-group", "target_type": 2}, ) db.session.commit() + db.session.expire_all() g_persnl_doc = get_doc_or_abort(g_persnl["id"]) g_docmnt_doc = get_doc_or_abort(g_docmnt["id"]) diff --git a/timApp/tests/server/timroutetest.py b/timApp/tests/server/timroutetest.py index 9589c1a761..7ade31485f 100644 --- a/timApp/tests/server/timroutetest.py +++ b/timApp/tests/server/timroutetest.py @@ -9,17 +9,24 @@ from contextlib import contextmanager from datetime import datetime from functools import lru_cache -from typing import Union, Any -from urllib.parse import urlparse +from typing import Union, Any, Generator import responses -from flask import Response, current_app -from flask import session +from flask import ( + Response, + current_app, + session, + has_app_context, + has_request_context, + g, +) +from flask.sessions import SessionMixin from flask.testing import FlaskClient from lxml import html from lxml.html import HtmlElement from requests import PreparedRequest from sqlalchemy import select +from sqlalchemy.orm import close_all_sessions, joinedload import timApp.tim from timApp.answer.answer import Answer @@ -45,8 +52,6 @@ MailmanMessageList, ) from timApp.messaging.messagelist.messagelist_models import MessageListModel -from timApp.plugin import containerLink -from timApp.plugin.containerLink import do_request from timApp.readmark.readparagraphtype import ReadParagraphType from timApp.tests.db.timdbtest import TimDbTest from timApp.tim_app import app @@ -81,9 +86,6 @@ def fast_getaddrinfo(host, port, family=0, addrtype=0, proto=0, flags=0): socket.getaddrinfo = fast_getaddrinfo -testclient: FlaskClient = timApp.tim.app.test_client() -testclient = testclient.__enter__() - def get_content(element: HtmlElement, selector: str = ".parContent") -> list[str]: return [r.text_content().strip() for r in element.cssselect(selector)] @@ -105,7 +107,7 @@ def get_cookie_value(resp: Response, key: str) -> str | None: return None -class TimRouteTest(TimDbTest): +class TimRouteTestBase(TimDbTest): """A base class for running tests for TIM routes.""" doc_num = 1 @@ -121,11 +123,31 @@ class TimRouteTest(TimDbTest): @classmethod def setUpClass(cls): super().setUpClass() - cls.client = testclient - # Default language on create_translation NOTE not same as british or - # american english. - cls.add_language("english") - db.session.commit() + with app.app_context(): + # Default language on create_translation NOTE not same as british or + # american english. + cls.add_language("english") + db.session.commit() + db.session.expire_all() + + def tearDown(self): + self.client.__exit__(None, None, None) + close_all_sessions() + + def _init_client(self) -> FlaskClient: + # Must be implemented by subclasses + raise NotImplementedError + + def setUp(self): + self.check_skip_tests() + # Create a default Flask client that holds the app context + self.client = self._init_client() + + # FIXME: It is a VERY bad idea to enter a client context for the duration of the entire test + # because the client is not multithreaded. See https://github.com/pallets/flask/issues/4734 + # Instead, the client contex should be entered only in specific tests and explicitly + self.client = self.client.__enter__() + self.client.open("/") @classmethod def add_language(cls, lang_name: str) -> Language: @@ -249,7 +271,9 @@ def request( headers: list[tuple[str, str]] | None = None, xhr=True, auth: BasicAuthParams | None = None, - force_return_text=False, + force_return_text: bool = False, + expire_session_after_request: bool = True, + client: FlaskClient | None = None, **kwargs, ) -> Response | str | dict: """Performs a request. @@ -275,83 +299,108 @@ def request( :param headers: Custom headers for the request. :param kwargs: Custom parameters to be passed to test client's 'open' method. Can be, for example, query_string={'a': '1', 'b': '2'} for passing URL arguments. + :param xhr: Whether to set the X-Requested-With header to XMLHttpRequest. + :param auth: Basic auth username and password as a tuple. + :param force_return_text: Whether to force returning the response as text. + :param expire_session_after_request: Whether to expire all session objects after the request. + :param client: The test client to use. If not provided, the default test client is used. :return: If as_tree is True: Returns the response as an HTML tree. Otherwise, if the response mimetype is application/json, returns the response as a JSON dict or list. Otherwise, returns the response as a string. """ - if headers is None: - headers = [] - if xhr: - headers.append(("X-Requested-With", "XMLHttpRequest")) - if auth: - u, p = auth - up = f"{u}:{p}".encode() - headers.append(("Authorization", f"Basic {b64encode(up).decode()}")) - resp = self.client.open(url, method=method, headers=headers, **kwargs) - is_textual = resp.mimetype in TEXTUAL_MIMETYPES - if expect_status is not None: - self.assertEqual( - expect_status, - resp.status_code, - msg=resp.get_data(as_text=True) if is_textual else None, - ) - if expect_mimetype is not None: - self.assertEqual(expect_mimetype, resp.mimetype) - if is_redirect(resp) and expect_content is not None: - self.assertEqual(expect_content, remove_prefix(resp.location, LOCALHOST)) - if expect_cookie is not None: - self.assertEqual(expect_cookie[1], get_cookie_value(resp, expect_cookie[0])) - resp_data = resp.get_data(as_text=is_textual) - if not is_textual: - return resp_data - if force_return_text: - return resp_data - if ( - expect_status >= 400 - and json_key is None - and (isinstance(expect_content, str) or isinstance(expect_contains, str)) - ): - json_key = "error" - if as_response: - return resp - if as_tree: - if json_key is not None: - resp_data = json.loads(resp_data)[json_key] - if as_tree is True: - tree = html.fromstring(resp_data) + + @contextmanager + def clean_db_after_request(): + try: + yield + finally: + if expire_session_after_request and has_app_context(): + db.session.remove() + # Reattach the user object to the session so that it can be tracked for changes + g.pop("user", None) + + with clean_db_after_request(): + if headers is None: + headers = [] + if xhr: + headers.append(("X-Requested-With", "XMLHttpRequest")) + if auth: + u, p = auth + up = f"{u}:{p}".encode() + headers.append(("Authorization", f"Basic {b64encode(up).decode()}")) + c = client or self.client + resp = c.open(url, method=method, headers=headers, **kwargs) + + is_textual = resp.mimetype in TEXTUAL_MIMETYPES + if expect_status is not None: + self.assertEqual( + expect_status, + resp.status_code, + msg=resp.get_data(as_text=True) if is_textual else None, + ) + if expect_mimetype is not None: + self.assertEqual(expect_mimetype, resp.mimetype) + if is_redirect(resp) and expect_content is not None: + self.assertEqual( + expect_content, remove_prefix(resp.location, LOCALHOST) + ) + if expect_cookie is not None: + self.assertEqual( + expect_cookie[1], get_cookie_value(resp, expect_cookie[0]) + ) + resp_data = resp.get_data(as_text=is_textual) + if not is_textual: + return resp_data + if force_return_text: + return resp_data + if ( + expect_status >= 400 + and json_key is None + and ( + isinstance(expect_content, str) or isinstance(expect_contains, str) + ) + ): + json_key = "error" + if as_response: + return resp + if as_tree: + if json_key is not None: + resp_data = json.loads(resp_data)[json_key] + if as_tree is True: + tree = html.fromstring(resp_data) + if expect_xpath is not None: + self.assertLessEqual(1, len(tree.findall(expect_xpath))) + elif as_tree == "fragments": + tree = html.fragments_fromstring(resp_data) + else: + raise Exception(f"Unknown value for as_tree: {as_tree}") + return tree + elif resp.mimetype == "application/json": + loaded = json.loads(resp_data) + if json_key is not None: + loaded = loaded[json_key] + if expect_content is not None: + self.assertEqual(expect_content, loaded) + if expect_contains is not None: + self.check_contains(expect_contains, loaded) if expect_xpath is not None: - self.assertLessEqual(1, len(tree.findall(expect_xpath))) - elif as_tree == "fragments": - tree = html.fragments_fromstring(resp_data) + self.assertIsNotNone(json_key) + self.assertLessEqual( + 1, + len( + html.fragment_fromstring( + loaded, create_parent=True + ).findall(expect_xpath) + ), + ) + return loaded else: - raise Exception(f"Unknown value for as_tree: {as_tree}") - return tree - elif resp.mimetype == "application/json": - loaded = json.loads(resp_data) - if json_key is not None: - loaded = loaded[json_key] - if expect_content is not None: - self.assertEqual(expect_content, loaded) - if expect_contains is not None: - self.check_contains(expect_contains, loaded) - if expect_xpath is not None: - self.assertIsNotNone(json_key) - self.assertLessEqual( - 1, - len( - html.fragment_fromstring(loaded, create_parent=True).findall( - expect_xpath - ) - ), - ) - return loaded - else: - if expect_content is not None and not is_redirect(resp): - self.assertEqual(expect_content, resp_data) - elif expect_contains is not None: - self.check_contains(expect_contains, resp_data) - return resp_data if not is_redirect(resp) else resp.location + if expect_content is not None and not is_redirect(resp): + self.assertEqual(expect_content, resp_data) + elif expect_contains is not None: + self.check_contains(expect_contains, resp_data) + return resp_data if not is_redirect(resp) else resp.location def check_contains(self, expect_contains, data): if isinstance(expect_contains, str): @@ -635,27 +684,31 @@ def post_answer( ref_from=None, expect_content=None, expect_status=200, + init_mock=None, **kwargs, ): - return self.json_put( - f"/{plugin_type}/{task_id}/answer", - { - "input": user_input, - "ref_from": {"docId": ref_from[0], "par": ref_from[1]} - if ref_from - else None, - "abData": { - "saveTeacher": save_teacher, - "teacher": teacher, - "userId": user_id, - "answer_id": answer_id, - "saveAnswer": True, + with self.internal_container_ctx() as m: + if init_mock: + init_mock(m) + return self.json_put( + f"/{plugin_type}/{task_id}/answer", + { + "input": user_input, + "ref_from": {"docId": ref_from[0], "par": ref_from[1]} + if ref_from + else None, + "abData": { + "saveTeacher": save_teacher, + "teacher": teacher, + "userId": user_id, + "answer_id": answer_id, + "saveAnswer": True, + }, }, - }, - expect_content=expect_content, - expect_status=expect_status, - **kwargs, - ) + expect_content=expect_content, + expect_status=expect_status, + **kwargs, + ) def post_answer_no_abdata( self, plugin_type, task_id, user_input, ref_from=None, **kwargs @@ -699,10 +752,9 @@ def current_group(self) -> UserGroup: return self.current_user.get_personal_group() def login_anonymous(self): - with self.client.session_transaction() as s: + with self.refreshing_session_transaction() as s: log_in_as_anonymous(s) - db.session.commit() - self.client.session_transaction().__enter__() + self.commit_db() def login_test1(self, force: bool = False, add: bool = False, **kwargs): """Logs testuser1 in. @@ -786,20 +838,19 @@ def login( :return: Response as a JSON dict. """ - if self.client.application.got_first_request and not manual: + if not manual: if not force and not add: u = User.get_by_name(username) - # if not flask.has_request_context(): - # print('creating request context') - # tim.app.test_request_context().__enter__() if not u: raise Exception(f"User not found: {username}") - with self.client.session_transaction() as s: + with self.refreshing_session_transaction() as s: s["user_id"] = u.id s.pop("other_users", None) - self.client.session_transaction().__enter__() + if has_request_context(): + # Force user object to refresh for the current request + g.pop("user", None) return - with self.client.session_transaction() as s: + with self.refreshing_session_transaction() as s: s.pop("last_doc", None) s.pop("came_from", None) return self.post( @@ -809,6 +860,20 @@ def login( **kwargs, ) + @contextmanager + def refreshing_session_transaction(self) -> Generator[SessionMixin, None, None]: + """A context manager that refreshes the active session context after the block is executed.""" + with self.client.session_transaction() as s: + yield s + if has_request_context(): + # If we are already in a request (or we had already one request), sync the session with the transaction + # This way any further calls to TIM API will reference the correct user + session_keys = set(session.keys()) + for k in session_keys: + if k not in session: + session.pop(k, None) + session.update(s) + def create_doc( self, path: str | None = None, @@ -861,6 +926,11 @@ def create_doc( self.assertIsInstance(resp["id"], int) self.assertEqual(path, resp["path"]) de = DocEntry.find_by_path(path) + + # After finding the document, modify the modifier group in case paragraphs will be added + # This will keep the edit log consistent + current_user_group_id = self.current_user.get_personal_group().id + de.document.modifier_group_id = current_user_group_id else: de = create_item_direct( item_path=path, @@ -870,10 +940,6 @@ def create_doc( template=template, cite=cite, ) - # TODO this isn't really correct but gives equivalent behavior compared to the True branch. - # The modifier should be corrected to be the current user in the True branch after - # calling DocEntry.find_by_path. After that, some tests need to be corrected. - de.document.modifier_group_id = 0 doc = de.document self.init_doc(doc, from_file, initial_par, settings) return de @@ -958,7 +1024,7 @@ def create_translation( expect_status=expect_status, **kwargs, ) - return db.session.get(Translation, j["id"]) if expect_status == 200 else None + return db.session.get(Translation, j["id"], options=[joinedload(Translation.docentry)]) if expect_status == 200 else None def assert_content(self, element: HtmlElement, expected: list[str]): pars = get_content(element) @@ -1036,7 +1102,7 @@ def get_no_warn(self, url: str, **kwargs): def make_admin(self, u: User): gr = UserGroup.get_admin_group() u.add_to_group(gr, added_by=None) - db.session.commit() + self.commit_db() def post_comment( self, @@ -1233,7 +1299,7 @@ def refresh_client(self): self.client.__enter__() @contextmanager - def internal_container_ctx(self): + def internal_container_ctx(self) -> Generator[responses.RequestsMock, None, None]: """Redirects internal container requests to go through Flask test client. Otherwise such requests would fail during test, unless BrowserTest class is used. @@ -1242,17 +1308,33 @@ def internal_container_ctx(self): """ with responses.RequestsMock(assert_all_requests_are_fired=False) as m: - def rq_cb(request: PreparedRequest, fn): - r: Response = fn( - request.path_url, - json_data=json.loads(request.body), - as_response=True, - content_type=request.headers.get( - "content-type", "application/octet-stream" - ), - ) + def rq_cb(request: PreparedRequest, fn, body_as_json: bool = True): + kwargs = {} + if body_as_json: + kwargs["json_data"] = json.loads(request.body) + with app.test_client() as c: + r: Response = fn( + request.path_url, + as_response=True, + content_type=request.headers.get( + "content-type", "application/octet-stream" + ), + client=c, + # Do not expire any sessions because there is likely an active session ongoing. + # This mock will be invoked likely by TIM calling internal plugin routes + # inside another routes. + # Because of the way Flask test client works, DB session is shared between + # the main route and the internal plugin routes. + # Closing a session inside internal plugin route may + # invalidate the objects in the main plugin route. + expire_session_after_request=False, + **kwargs, + ) return r.status_code, {}, r.data + def rq_cb_get(request: PreparedRequest): + return rq_cb(request, self.get, body_as_json=False) + def rq_cb_put(request: PreparedRequest): return rq_cb(request, self.json_put) @@ -1260,6 +1342,9 @@ def rq_cb_post(request: PreparedRequest): return rq_cb(request, self.json_post) host = current_app.config["INTERNAL_PLUGIN_DOMAIN"] + m.add_callback( + "GET", re.compile(f"http://{host}:5001/"), callback=rq_cb_get + ) m.add_callback( "PUT", re.compile(f"http://{host}:5001/"), callback=rq_cb_put ) @@ -1269,73 +1354,30 @@ def rq_cb_post(request: PreparedRequest): m.add_passthru("http://csplugin:5000") m.add_passthru("http://jsrunner:5000") m.add_passthru("http://fields:5000") - yield - - @contextmanager - def importdata_ctx(self, aalto_return=None): - with responses.RequestsMock() as m: - if aalto_return: - m.add( - "GET", - "https://plus.cs.aalto.fi/api/v2/courses/1234/aggregatedata/?format=json", - body=json.dumps(aalto_return), - status=200, - ) - - def rq_cb(request: PreparedRequest): - r = self.json_put(request.path_url, json_data=json.loads(request.body)) - return 200, {}, json.dumps(r) - - host = current_app.config["INTERNAL_PLUGIN_DOMAIN"] - m.add_callback( - "PUT", f"http://{host}:5001/importData/answer", callback=rq_cb - ) - m.add_passthru("http://jsrunner:5000") - yield + m.add_passthru("http://dumbo:5000") + m.add_passthru("http://pali:5000") + m.add_passthru("http://feedback:5000") + m.add_passthru("http://haskellplugins:5002") + m.add_passthru("http://showfile:5000") + m.add_passthru("http://mailman-test:8001") + yield m @contextmanager def temp_config(self, settings: dict[str, Any]): - old_settings = {k: current_app.config[k] for k in settings.keys()} + old_settings = {k: app.config[k] for k in settings.keys()} for k, v in settings.items(): - current_app.config[k] = v + app.config[k] = v try: yield finally: for k, v in old_settings.items(): - current_app.config[k] = v + app.config[k] = v -class TimPluginFix(TimRouteTest): - """Unused class. This was a test whether local plugins could be made to work without BrowserTest class.""" +class TimRouteTest(TimRouteTestBase): - def setUp(self): - super().setUp() - - # Some plugins live in TIM container, which means we cannot use the requests library to call those plugins - # because there is no real server running (it is just the test client). - # Here we replace the request method in containerLink so that all such requests are redirected - # to the test client. - def test_do_request(method: str, url: str, data, params, headers, read_timeout): - parsed = urlparse(url) - if parsed.hostname != "localhost": - return do_request(method, url, data, params, headers, read_timeout) - r = self.request( - url=parsed.path, - method=method, - headers=[(k, v) for k, v in headers.items()] if headers else None, - data=data, - query_string=params, - force_return_text=True, - follow_redirects=True, - ) - testclient.__exit__(None, None, None) - return r - - containerLink.plugin_request_fn = test_do_request - - def tearDown(self): - super().tearDown() - containerLink.plugin_request_fn = do_request + def _init_client(self) -> FlaskClient: + return timApp.tim.app.test_client() class TimMessageListTest(TimRouteTest): diff --git a/timApp/tests/timliveserver.py b/timApp/tests/timliveserver.py deleted file mode 100644 index a47e16d18b..0000000000 --- a/timApp/tests/timliveserver.py +++ /dev/null @@ -1,16 +0,0 @@ -from flask_testing import LiveServerTestCase - - -class TimLiveServer(LiveServerTestCase): - def create_app(self): - from timApp.tim_app import app - - return app - - def setUp(self): - super().setUp() - self.client = self.app.test_client() - self.client.__enter__() - - def tearDown(self): - self.client.__exit__(None, None, None) diff --git a/timApp/tim_app.py b/timApp/tim_app.py index caf7f9fea6..0f25526ac5 100644 --- a/timApp/tim_app.py +++ b/timApp/tim_app.py @@ -254,12 +254,6 @@ setup_logging(app) # Compress(app) - -# Disabling object expiration on commit makes testing easier -# because sometimes objects would expire after calling a route. -if app.config["TESTING"]: - db.session = db.create_scoped_session({"expire_on_commit": False}) - db.init_app(app) db.app = app migrate = Migrate(app, db) @@ -307,7 +301,7 @@ def print_schema(bind: str | None = None): models = inspect.getmembers( sys.modules[__name__], lambda x: inspect.isclass(x) and hasattr(x, "__table__") ) - eng = db.get_engine(app, bind) + eng = db.engines[bind] for _, model_class in models: print(CreateTable(model_class.__table__).compile(eng), end=";") diff --git a/timApp/timdb/dbaccess.py b/timApp/timdb/dbaccess.py index 9d8bb562ff..261d3c3d65 100644 --- a/timApp/timdb/dbaccess.py +++ b/timApp/timdb/dbaccess.py @@ -2,22 +2,6 @@ from pathlib import Path -# from timApp.timdb.timdb import TimDb -# -# -# def get_timdb() -> TimDb: -# """Returns the TimDb object and stores it in the Flask g object.""" -# if not hasattr(g, "timdb"): -# from timApp.auth.sessioninfo import get_current_user_object -# -# g.timdb = TimDb( -# files_root_path=get_files_path(), -# current_user_name=get_current_user_object().name, -# route_path=request.path, -# ) -# return g.timdb - - @cache def get_files_path() -> Path: from timApp.tim_app import app diff --git a/timApp/timdb/init.py b/timApp/timdb/init.py index 49d397750a..310ef5a042 100644 --- a/timApp/timdb/init.py +++ b/timApp/timdb/init.py @@ -9,7 +9,6 @@ from alembic.runtime.environment import EnvironmentContext from alembic.runtime.migration import MigrationContext from alembic.script import ScriptDirectory -from sqlalchemy.orm import Session from sqlalchemy_utils import database_exists, create_database from timApp.admin.language_cli import add_all_supported_languages @@ -30,10 +29,7 @@ from timApp.messaging.messagelist.messagelist_utils import MESSAGE_LIST_DOC_PREFIX from timApp.plugin.calendar.models import EnrollmentType from timApp.tim_app import app -from timApp.timdb.dbaccess import get_files_path from timApp.timdb.sqa import db, get_tim_main_engine - -# from timApp.timdb.timdb import TimDb from timApp.user.settings.style_utils import ( OFFICIAL_STYLES_PATH, USER_STYLES_PATH, @@ -72,13 +68,10 @@ def database_has_tables(): def initialize_database(create_docs: bool = True) -> None: - files_root_path = get_files_path() db_uri = app.config["DB_URI"] was_created = postgre_create_database(db_uri) if was_created: log_info(f"Database {db_uri} was created.") - # timdb = TimDb(files_root_path=files_root_path) - # sess = timdb.session with app.app_context(): sess = db.session @@ -174,7 +167,7 @@ def initialize_database(create_docs: bool = True) -> None: BlockType.Document, ) - verify_contact_message_template = import_document_from_file( + _ = import_document_from_file( static_tim_doc("initial/contact_verify_message.md"), "settings/verify-templates/contact", admin_group, @@ -206,7 +199,6 @@ def initialize_database(create_docs: bool = True) -> None: if not app.config["TESTING"]: exit_if_not_db_up_to_date() - # timdb.close() def create_style_docs() -> tuple[list[Folder], list[DocInfo]]: diff --git a/timApp/timdb/sqa.py b/timApp/timdb/sqa.py index 85b4cd2715..ef3c70d9e5 100644 --- a/timApp/timdb/sqa.py +++ b/timApp/timdb/sqa.py @@ -6,12 +6,24 @@ Use Flask-Migrate for database migrations. See . """ +import os from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import func +from sqlalchemy import func, text from sqlalchemy.orm.base import instance_state -db = SQLAlchemy() +session_options = { + "future": True, +} +engine_options = { + "future": True, +} +if os.environ.get("TIM_TESTING", None): + # Disabling object expiration on commit makes testing easier + # because sometimes objects would expire after calling a route. + session_options["expire_on_commit"] = False + +db = SQLAlchemy(session_options=session_options, engine_options=engine_options) class TimeStampMixin: @@ -24,18 +36,12 @@ class TimeStampMixin: ) -# UserGroupMember = db.Table('usergroupmember', -# db.Column('usergroup_id', db.Integer, db.ForeignKey('usergroup.id'), primary_key=True), -# db.Column('user_id', db.Integer, db.ForeignKey('useraccount.id'), primary_key=True), -# ) - - def tim_main_execute(sql: str, params=None): - return db.session.execute(sql, params, bind=get_tim_main_engine()) + return db.session.execute(text(sql), params, bind_arguments={"bind": get_tim_main_engine()}) def get_tim_main_engine(): - return db.get_engine() + return db.engine def include_if_loaded(attr_name: str, obj, key_name=None): diff --git a/timApp/upload/upload.py b/timApp/upload/upload.py index 6e11c23e56..59a50a7214 100644 --- a/timApp/upload/upload.py +++ b/timApp/upload/upload.py @@ -1,4 +1,3 @@ -import imghdr import io import json import os @@ -51,6 +50,7 @@ is_script_safe_mimetype, ) from timApp.user.user import User +from timApp.util.file_utils import guess_image_type from timApp.util.flask.requesthelper import ( use_model, RouteException, @@ -594,7 +594,7 @@ def upload_image_or_file(d: DocInfo, file): def upload_image_or_file_impl(d: DocInfo, file): content = file.read() - imgtype = imghdr.what(None, h=content) + imgtype = guess_image_type(content) type_str = "image" if imgtype else "file" f = save_file_and_grant_access(d, content, file, BlockType.from_str(type_str)) return f, type_str @@ -688,12 +688,11 @@ def get_image(image_id: str, image_filename: str) -> Response: verify_view_access(f, check_parents=True) if image_filename != f.filename: raise NotExist("Image not found") - img_data = f.data - imgtype = imghdr.what(None, h=img_data) + imgtype = guess_image_type(f.filesystem_path) # Redirect if we can't deduce the image type if not imgtype: return safe_redirect( url_for("upload.get_file", file_id=image_id, file_filename=image_filename) ) - f = io.BytesIO(img_data) + f = io.BytesIO(f.data) return send_file(f, mimetype="image/" + imgtype) diff --git a/timApp/user/consentchange.py b/timApp/user/consentchange.py index 4a768cfaf1..b6f6faa77a 100644 --- a/timApp/user/consentchange.py +++ b/timApp/user/consentchange.py @@ -6,6 +6,8 @@ class ConsentChange(db.Model): __tablename__ = "consentchange" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) time = db.Column(db.DateTime(timezone=True), nullable=False, default=func.now()) diff --git a/timApp/user/groups.py b/timApp/user/groups.py index b329e30a31..b61410c82a 100644 --- a/timApp/user/groups.py +++ b/timApp/user/groups.py @@ -73,7 +73,7 @@ def get_uid_gid( .all() ) group = ( - db.sesion.execute(select(UserGroup).filter_by(name=group_name).limit(1)) + db.session.execute(select(UserGroup).filter_by(name=group_name).limit(1)) .scalars() .first() ) diff --git a/timApp/user/hakaorganization.py b/timApp/user/hakaorganization.py index 8cea0a281f..713b3c1f10 100644 --- a/timApp/user/hakaorganization.py +++ b/timApp/user/hakaorganization.py @@ -7,6 +7,8 @@ class HakaOrganization(db.Model): + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Text, nullable=False, unique=True) diff --git a/timApp/user/newuser.py b/timApp/user/newuser.py index da2db74852..2f06c33f21 100644 --- a/timApp/user/newuser.py +++ b/timApp/user/newuser.py @@ -8,6 +8,8 @@ class NewUser(db.Model): """A user that is going to register to TIM via email and has not yet completed the registration process.""" __tablename__ = "newuser" + __allow_unmapped__ = True + email = db.Column(db.Text, primary_key=True) """Email address.""" diff --git a/timApp/user/personaluniquecode.py b/timApp/user/personaluniquecode.py index a9c15e3943..85b4045883 100644 --- a/timApp/user/personaluniquecode.py +++ b/timApp/user/personaluniquecode.py @@ -11,6 +11,8 @@ class PersonalUniqueCode(db.Model): """The database model for the 'schacPersonalUniqueCode' Haka attribute.""" + __allow_unmapped__ = True + user_id = db.Column( db.Integer, db.ForeignKey("useraccount.id"), nullable=False, primary_key=True ) diff --git a/timApp/user/user.py b/timApp/user/user.py index c34cc40763..cd802ed53d 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -248,6 +248,8 @@ class User(db.Model, TimeStampMixin, SCIMEntity): """ __tablename__ = "useraccount" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) """User identifier.""" @@ -328,11 +330,15 @@ def _set_email(self, value: str) -> None: back_populates="user", lazy="select", overlaps="primary_email_contact", + cascade_backrefs=False, ) """User's contacts.""" notifications = db.relationship( - "Notification", back_populates="user", lazy="dynamic" + "Notification", + back_populates="user", + lazy="dynamic", + cascade_backrefs=False, ) """Notification settings for the user. Represents what notifications the user wants to receive.""" @@ -407,7 +413,7 @@ def _set_email(self, value: str) -> None: """Lecture messages that the user sent to lectures as a dynamic query.""" questionactivity = db.relationship( - "QuestionActivity", back_populates="user", lazy="select" + "QuestionActivity", back_populates="user", lazy="select", cascade_backrefs=False ) """User's activity on lecture questions.""" @@ -420,6 +426,7 @@ def _set_email(self, value: str) -> None: back_populates="users", lazy="dynamic", overlaps="users_all", + cascade_backrefs=False, ) """User's answers to tasks as a dynamic query.""" @@ -432,7 +439,7 @@ def _set_email(self, value: str) -> None: """Velps created by the user as a dynamic query.""" sessions: list[UserSession] = db.relationship( - "UserSession", back_populates="user", lazy="select" + "UserSession", back_populates="user", lazy="select", cascade_backrefs=False ) """All user's sessions as a dynamic query.""" @@ -455,6 +462,7 @@ def _set_email(self, value: str) -> None: "Answer", secondary=UserAnswer.__table__, overlaps="answers, users", + cascade_backrefs=False, ) annotations_alt = db.relationship("Annotation", overlaps="annotations, annotator") velps_alt = db.relationship("Velp", overlaps="velps, creator") @@ -487,11 +495,17 @@ def update_email( self._email = new_email if prev_email != new_email: if create_contact: - new_primary = db.session.scalars( - select(UserContact).filter_by( - user_id=self.id, channel=Channel.EMAIL, contact=prev_email + new_primary = ( + db.session.execute( + select(UserContact) + .filter_by( + user_id=self.id, channel=Channel.EMAIL, contact=new_email + ) + .limit(1) ) - ).one_or_none() + .scalars() + .first() + ) if not new_primary: # If new primary contact does not exist for the email, create it # This is used mainly for CLI operations where email of the user is changed directly @@ -617,7 +631,7 @@ def effective_real_groups(): locked_groups = get_locked_active_groups() return locked_groups if locked_groups is not None else effective_real_groups() - @cached_property + @property def is_admin(self): return get_admin_group_id() in self.effective_group_ids @@ -703,9 +717,9 @@ def get_by_id(uid: int) -> Optional["User"]: def get_by_email(email: str) -> Optional["User"]: if email is None: raise Exception("Tried to find an user by null email") - return db.session.scalars( + return db.session.execute( user_query_with_joined_groups().filter_by(email=email).limit(1) - ).first() + ).scalars().first() @staticmethod def get_by_email_case_insensitive(email: str) -> list["User"]: @@ -817,11 +831,11 @@ def _get_personal_folders(self) -> list[Folder]: .options( defaultload(Folder._block) .selectinload(Block.accesses) - .selectinload(BlockAccess.usergroup) + .joinedload(BlockAccess.usergroup) ) ) - return db.session.scalars(stmt).unique().all() + return db.session.scalars(stmt).all() @cached_property def personal_folder_prop(self) -> Folder: @@ -863,17 +877,15 @@ def get_groups( special_groups = [ANONYMOUS_GROUPNAME] if self.logged_in: special_groups.append(LOGGED_IN_GROUPNAME) - filter_expr = UserGroupMember.user_id == self.id + member_condition = UserGroupMember.user_id == self.id if not include_expired: - filter_expr = filter_expr & membership_current - stmt = select(UserGroup).filter( - UserGroup.id.in_(select(UserGroupMember.usergroup_id).filter(filter_expr)) + member_condition = member_condition & membership_current + group_condition = UserGroup.id.in_( + select(UserGroupMember.usergroup_id).filter(member_condition) ) if include_special: - stmt = stmt.union( - select(UserGroup).filter(UserGroup.name.in_(special_groups)) - ) - return stmt + group_condition = group_condition | UserGroup.name.in_(special_groups) + return select(UserGroup).filter(group_condition) def add_to_group( self, @@ -904,7 +916,9 @@ def add_to_group( existing.membership_end = None new_add = False else: - self.memberships.append(UserGroupMember(group=ug, adder=added_by)) + ugm = UserGroupMember(group=ug, adder=added_by) + self.memberships.append(ugm) + db.session.add(ugm) new_add = True # On changing of group, sync this person to the user group's message lists. if sync_mailing_lists: diff --git a/timApp/user/usercontact.py b/timApp/user/usercontact.py index b644d19fc2..00a14688e3 100644 --- a/timApp/user/usercontact.py +++ b/timApp/user/usercontact.py @@ -32,6 +32,7 @@ class UserContact(db.Model): """TIM users' additional contact information.""" __tablename__ = "usercontact" + __allow_unmapped__ = True __table_args__ = ( # A user should not have the same contact for the channel diff --git a/timApp/user/usergroup.py b/timApp/user/usergroup.py index b2c77f192b..e573fa2efb 100644 --- a/timApp/user/usergroup.py +++ b/timApp/user/usergroup.py @@ -61,6 +61,8 @@ class UserGroup(db.Model, TimeStampMixin, SCIMEntity): """ __tablename__ = "usergroup" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) """Usergroup identifier.""" @@ -110,12 +112,14 @@ def scim_display_name(self): "BlockAccess", back_populates="usergroup", lazy="dynamic", + cascade_backrefs=False, ) accesses_alt: dict[tuple[int, int], BlockAccess] = db.relationship( "BlockAccess", collection_class=attribute_mapped_collection("group_collection_key"), cascade="all, delete-orphan", overlaps="accesses, usergroup", + cascade_backrefs=False, ) readparagraphs = db.relationship( "ReadParagraph", back_populates="usergroup", lazy="dynamic" @@ -124,7 +128,7 @@ def scim_display_name(self): "ReadParagraph", overlaps="readparagraphs, usergroup", ) - notes = db.relationship("UserNote", back_populates="usergroup", lazy="dynamic") + notes = db.relationship("UserNote", back_populates="usergroup", lazy="dynamic", cascade_backrefs=False) notes_alt = db.relationship("UserNote", overlaps="notes, usergroup") admin_doc: Block = db.relationship( @@ -144,7 +148,7 @@ def scim_display_name(self): ) internalmessage_display: InternalMessageDisplay | None = db.relationship( - "InternalMessageDisplay", back_populates="usergroup" + "InternalMessageDisplay", back_populates="usergroup", cascade_backrefs=False ) def __repr__(self): diff --git a/timApp/user/usergroupdoc.py b/timApp/user/usergroupdoc.py index ba69f8ac5d..e9d16de73c 100644 --- a/timApp/user/usergroupdoc.py +++ b/timApp/user/usergroupdoc.py @@ -7,5 +7,7 @@ class UserGroupDoc(db.Model): """ __tablename__ = "usergroupdoc" + __allow_unmapped__ = True + group_id = db.Column(db.Integer, db.ForeignKey("usergroup.id"), primary_key=True) doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) diff --git a/timApp/user/usergroupmember.py b/timApp/user/usergroupmember.py index d04b8ab207..ff38b4214b 100644 --- a/timApp/user/usergroupmember.py +++ b/timApp/user/usergroupmember.py @@ -24,6 +24,7 @@ class UserGroupMember(db.Model): """ __tablename__ = "usergroupmember" + __allow_unmapped__ = True usergroup_id = db.Column( db.Integer, db.ForeignKey("usergroup.id"), primary_key=True diff --git a/timApp/user/userutils.py b/timApp/user/userutils.py index d026fd81c0..a4c4c2053f 100644 --- a/timApp/user/userutils.py +++ b/timApp/user/userutils.py @@ -196,6 +196,7 @@ def grant_access( duration=duration, require_confirm=require_confirm, ) + db.session.add(ba) group.accesses_alt[key] = ba return ba diff --git a/timApp/user/verification/verification.py b/timApp/user/verification/verification.py index bfcfd520e2..ee4eedf58a 100644 --- a/timApp/user/verification/verification.py +++ b/timApp/user/verification/verification.py @@ -50,6 +50,7 @@ class Verification(db.Model): verification.""" __tablename__ = "verification" + __allow_unmapped__ = True token = db.Column(db.Text, primary_key=True) """Verification token used for action verification""" diff --git a/timApp/util/file_utils.py b/timApp/util/file_utils.py new file mode 100644 index 0000000000..99fc4299ba --- /dev/null +++ b/timApp/util/file_utils.py @@ -0,0 +1,26 @@ +from mimetypes import guess_extension +from os import PathLike + +import magic + + +def guess_image_type(p: PathLike | str | bytes) -> str | None: + """ + Guess the image type of file. + + If the file is not an image, return None. + + :param p: Path to file or bytes content of file. + :return: Image type or None. + """ + m = magic.Magic(mime=True) + + if isinstance(p, bytes): + mime = m.from_buffer(p) + else: + mime = m.from_file(p) + + if not mime.startswith("image/"): + return None + + return guess_extension(mime).lstrip(".") diff --git a/timApp/util/get_fields.py b/timApp/util/get_fields.py index a3108f9413..023847e387 100644 --- a/timApp/util/get_fields.py +++ b/timApp/util/get_fields.py @@ -358,15 +358,11 @@ def get_fields_and_users( elif user_filter is not None: # Ensure user filter gets applied even if group filter is skipped in include_all_answered q = q.filter(user_filter) - sub += ( - db.session.execute( - q.group_by(Answer.task_id, User.id).with_only_columns( - func.max(Answer.id), User.id - ) + sub += db.session.execute( + q.group_by(Answer.task_id, User.id).with_only_columns( + func.max(Answer.id), User.id ) - .scalars() - .all() - ) + ).all() aid_uid_map = defaultdict(list) user_ids = set() for aid, uid in sub: @@ -382,7 +378,8 @@ def get_fields_and_users( if user_filter is not None: id_filter = id_filter & user_filter q2 = select(User).filter(id_filter) - q = q1.union(q2) + q = q1.with_only_columns(User.id).union(q2.with_only_columns(User.id)) + q = select(User).filter(User.id.in_(q)) else: q = q1 q = q.with_only_columns(User).order_by(User.id).options(lazyload(User.groups)) @@ -432,20 +429,16 @@ def get_fields_and_users( for u in users: counts[u.id] = {} cnt = func.count(Answer.id).label("cnt") - answer_counts = ( - db.session.execute( - select(Answer) - .filter( - Answer.task_id.in_([tid.doc_task for tid in tasks_with_count_field]) - ) - .join(User, Answer.users) - .filter(User.id.in_([u.id for u in users])) - .group_by(User.id, Answer.task_id) - .with_only_columns(User.id, Answer.task_id, cnt) + answer_counts = db.session.execute( + select(Answer) + .filter( + Answer.task_id.in_([tid.doc_task for tid in tasks_with_count_field]) ) - .scalars() - .all() - ) + .join(User, Answer.users) + .filter(User.id.in_([u.id for u in users])) + .group_by(User.id, Answer.task_id) + .with_only_columns(User.id, Answer.task_id, cnt) + ).all() for uid, taskid, count in answer_counts: counts[uid][taskid] = count diff --git a/timApp/velp/annotation_model.py b/timApp/velp/annotation_model.py index 52bee96419..6588d0188e 100644 --- a/timApp/velp/annotation_model.py +++ b/timApp/velp/annotation_model.py @@ -44,6 +44,8 @@ class Annotation(db.Model): """ __tablename__ = "annotation" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) """Annotation identifier.""" diff --git a/timApp/velp/annotations.py b/timApp/velp/annotations.py index 5f5373a23e..47b9098238 100644 --- a/timApp/velp/annotations.py +++ b/timApp/velp/annotations.py @@ -10,7 +10,7 @@ from enum import Enum, unique from sqlalchemy import func, true, select -from sqlalchemy.orm import selectinload, contains_eager +from sqlalchemy.orm import selectinload, joinedload from sqlalchemy.sql import Select from timApp.answer.answer import Answer @@ -64,15 +64,13 @@ def get_annotations_with_comments_in_document( if is_peerreview_enabled(d): answer_filter |= User.id.in_( get_reviews_where_user_is_reviewer_query(d, user) - .with_entities(PeerReview.reviewable_id) - .subquery() + .with_only_columns(PeerReview.reviewable_id) ) own_review_filter = (User.id == user.id) | ( Annotation.annotator_id == user.id ) - anns = ( - db.session.execute( - set_annotation_query_opts( + + q = (set_annotation_query_opts( select(Annotation) .filter_by(document_id=d.id) .filter( @@ -94,12 +92,15 @@ def get_annotations_with_comments_in_document( Annotation.offset_start.desc(), ) ) - .options(contains_eager(Annotation.velp_content)) - .options(contains_eager(Annotation.answer).contains_eager(Answer.users_all)) + .options(joinedload(Annotation.velp_content)) + .options(joinedload(Annotation.answer).selectinload(Answer.users_all)) .options( - contains_eager(Annotation.velp_version).contains_eager(VelpVersion.velp) + joinedload(Annotation.velp_version).joinedload(VelpVersion.velp) ) - .with_only_columns(Annotation) + .with_only_columns(Annotation)) + anns = ( + db.session.execute( + q ) .scalars() .all() @@ -112,19 +113,19 @@ def set_annotation_query_opts(q: Select) -> Select: q.options(selectinload(Annotation.velp_content).load_only(VelpContent.content)) .options( selectinload(Annotation.comments) - .selectinload(AnnotationComment.commenter) + .joinedload(AnnotationComment.commenter) .raiseload(User.groups) ) .options(selectinload(Annotation.annotator).raiseload(User.groups)) .options( - selectinload(Annotation.answer) + joinedload(Annotation.answer) .selectinload(Answer.users_all) .raiseload(User.groups) ) .options( selectinload(Annotation.velp_version) .load_only(VelpVersion.id, VelpVersion.velp_id) - .selectinload(VelpVersion.velp) + .joinedload(VelpVersion.velp) .load_only(Velp.color) ) ) diff --git a/timApp/velp/velp_models.py b/timApp/velp/velp_models.py index 49ed6c7b7b..3f0a5e0813 100644 --- a/timApp/velp/velp_models.py +++ b/timApp/velp/velp_models.py @@ -11,6 +11,8 @@ class VelpContent(db.Model): """The actual content of a Velp.""" __tablename__ = "velpcontent" + __allow_unmapped__ = True + version_id = db.Column( db.Integer, db.ForeignKey("velpversion.id"), primary_key=True ) @@ -25,6 +27,8 @@ class AnnotationComment(db.Model): """A comment in an Annotation.""" __tablename__ = "annotationcomment" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) """Comment identifier.""" @@ -62,12 +66,16 @@ class LabelInVelp(db.Model): """Associates VelpLabels with Velps.""" __tablename__ = "labelinvelp" + __allow_unmapped__ = True + label_id = db.Column(db.Integer, db.ForeignKey("velplabel.id"), primary_key=True) velp_id = db.Column(db.Integer, db.ForeignKey("velp.id"), primary_key=True) class VelpInGroup(db.Model): __tablename__ = "velpingroup" + __allow_unmapped__ = True + velp_group_id = db.Column( db.Integer, db.ForeignKey("velpgroup.id"), primary_key=True ) @@ -78,6 +86,8 @@ class Velp(db.Model): """A Velp is a kind of category for Annotations and is visually represented by a Post-it note.""" __tablename__ = "velp" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) creator_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) creation_time = db.Column( @@ -130,6 +140,8 @@ class VelpGroup(db.Model): """Represents a group of Velps.""" __tablename__ = "velpgroup" + __allow_unmapped__ = True + id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) name = db.Column(db.Text) creation_time = db.Column( @@ -164,6 +176,8 @@ def to_json(self) -> dict: class VelpGroupDefaults(db.Model): __tablename__ = "velpgroupdefaults" + __allow_unmapped__ = True + doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) target_type = db.Column( db.Integer, nullable=False @@ -179,12 +193,16 @@ class VelpGroupLabel(db.Model): """Currently not used (0 rows in production DB as of 5th July 2018).""" __tablename__ = "velpgrouplabel" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) content = db.Column(db.Text, nullable=False) class VelpGroupSelection(db.Model): __tablename__ = "velpgroupselection" + __allow_unmapped__ = True + user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) target_type = db.Column( @@ -205,6 +223,8 @@ class VelpGroupsInDocument(db.Model): """ __tablename__ = "velpgroupsindocument" + __allow_unmapped__ = True + user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) velp_group_id = db.Column( @@ -216,6 +236,8 @@ class VelpLabel(db.Model): """A label that can be assigned to a Velp.""" __tablename__ = "velplabel" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) # TODO make not nullable creator_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=True) @@ -231,6 +253,8 @@ class VelpLabel(db.Model): class VelpLabelContent(db.Model): __tablename__ = "velplabelcontent" + __allow_unmapped__ = True + velplabel_id = db.Column( db.Integer, db.ForeignKey("velplabel.id"), primary_key=True ) @@ -249,6 +273,8 @@ def to_json(self) -> dict: class VelpVersion(db.Model): __tablename__ = "velpversion" + __allow_unmapped__ = True + id = db.Column(db.Integer, primary_key=True) velp_id = db.Column(db.Integer, db.ForeignKey("velp.id"), nullable=False) modify_time = db.Column( diff --git a/timApp/velp/velps.py b/timApp/velp/velps.py index dd6f856876..7bfb6accaf 100644 --- a/timApp/velp/velps.py +++ b/timApp/velp/velps.py @@ -252,9 +252,9 @@ def get_velp_content_for_document( ) .with_only_columns(Velp) .options(selectinload(Velp.groups).raiseload(VelpGroup.block)) - .options(selectinload(Velp.velp_versions).selectinload(VelpVersion.content)) + .options(selectinload(Velp.velp_versions).joinedload(VelpVersion.content)) ) - return vq.all() + return db.session.execute(vq).scalars().all() def get_velp_label_content_for_document( diff --git a/tim_common/timjsonencoder.py b/tim_common/timjsonencoder.py index 2bf24d29ef..93d2b4e437 100644 --- a/tim_common/timjsonencoder.py +++ b/tim_common/timjsonencoder.py @@ -56,7 +56,7 @@ def default(self, o): f for f in flds if not f.startswith("_") - and f not in ["metadata", "query", "query_class"] + and f not in ["metadata", "query", "query_class", "registry"] ] for field in flds: value = o.__getattribute__(field) From 947343bebe901b54a1856bdc32487ded5c3aceb7 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Wed, 26 Jul 2023 21:36:08 +0300 Subject: [PATCH 06/34] Bump SQLAlchemy to 2.0 --- cli/commands/dev/build.py | 30 +- poetry.lock | 1394 +++++++++++++++++++------------------ pyproject.toml | 8 +- 3 files changed, 756 insertions(+), 676 deletions(-) diff --git a/cli/commands/dev/build.py b/cli/commands/dev/build.py index 3730793db4..1a64ba790c 100644 --- a/cli/commands/dev/build.py +++ b/cli/commands/dev/build.py @@ -27,7 +27,7 @@ # 2. Specify the task and valid tags in the BUILD_TASKS dictionary. -def build_tim(tag: Optional[str], no_cache: bool) -> Optional[List[str]]: +def build_tim(tag: Optional[str], no_cache: bool, build_args: List[str]) -> Optional[List[str]]: config = get_config() image_suffix = "-base" if tag == "base" else "" image_name = f"{config.images_repository}/tim{image_suffix}" @@ -35,10 +35,16 @@ def build_tim(tag: Optional[str], no_cache: bool) -> Optional[List[str]]: image_name_latest = f"{image_name}:latest" cwd = Path.cwd() dockerfile = cwd / "timApp" / "Dockerfile" + build_args_cli = [] + if build_args: + for arg in build_args: + build_args_cli.append("--build-arg") + build_args_cli.append(arg) run_docker( [ "build", *(["--no-cache"] if no_cache else []), + *build_args_cli, "--tag", image_name_specific, "--tag", @@ -51,15 +57,21 @@ def build_tim(tag: Optional[str], no_cache: bool) -> Optional[List[str]]: return [image_name_specific, image_name_latest] -def build_csplugin(tag: Optional[str], no_cache: bool) -> Optional[List[str]]: +def build_csplugin(tag: Optional[str], no_cache: bool, build_args: List[str]) -> Optional[List[str]]: assert tag is not None config = get_config() image_name = f"{config.images_repository}/cs3:{tag}-{csplugin_image_tag()}" context = Path.cwd() / "timApp" / "modules" / "cs" + build_args_cli = [] + if build_args: + for arg in build_args: + build_args_cli.append("--build-arg") + build_args_cli.append(arg) run_docker( [ "build", *(["--no-cache"] if no_cache else []), + *build_args_cli, "--tag", image_name, "--target", @@ -76,12 +88,13 @@ def build_csplugin(tag: Optional[str], no_cache: bool) -> Optional[List[str]]: class Arguments: push: bool no_cache: bool + build_args: List[str] tasks: List[str] class BuildTask(NamedTuple): tags: Optional[List[str]] - build: Callable[[Optional[str], bool], Optional[List[str]]] + build: Callable[[Optional[str], bool, List[str]], Optional[List[str]]] BUILD_TASKS: Dict[str, BuildTask] = { @@ -104,12 +117,12 @@ def run(args: Arguments) -> None: if build_tags: for tag in build_tags: log_info(f"Building {task}:{tag}") - images = build_task.build(tag, args.no_cache) + images = build_task.build(tag, args.no_cache, args.build_args) if images: built_images.extend(images) else: log_info(f"Building {task}") - images = build_task.build(None, args.no_cache) + images = build_task.build(None, args.no_cache, args.build_args) if images: built_images.extend(images) @@ -137,9 +150,16 @@ def init(parser: ArgumentParser) -> None: action="store_true", dest="no_cache", ) + parser.add_argument( + "--build-arg", + help="Build arguments to pass to the Docker build command.", + action="append", + dest="build_args", + ) parser.add_argument( "tasks", nargs="*", choices=choices, help="Tasks to build in format `task:tag`. If not specified, all tasks will be built.", ) + diff --git a/poetry.lock b/poetry.lock index 799f0b2888..0455c6efe5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,98 +2,98 @@ [[package]] name = "aiohttp" -version = "3.8.4" +version = "3.8.5" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.6" files = [ - {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"}, - {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"}, - {file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea"}, - {file = "aiohttp-3.8.4-cp310-cp310-win32.whl", hash = "sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1"}, - {file = "aiohttp-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24"}, - {file = "aiohttp-3.8.4-cp311-cp311-win32.whl", hash = "sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d"}, - {file = "aiohttp-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc"}, - {file = "aiohttp-3.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff"}, - {file = "aiohttp-3.8.4-cp36-cp36m-win32.whl", hash = "sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777"}, - {file = "aiohttp-3.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e"}, - {file = "aiohttp-3.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241"}, - {file = "aiohttp-3.8.4-cp37-cp37m-win32.whl", hash = "sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a"}, - {file = "aiohttp-3.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d"}, - {file = "aiohttp-3.8.4-cp38-cp38-win32.whl", hash = "sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54"}, - {file = "aiohttp-3.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4"}, - {file = "aiohttp-3.8.4-cp39-cp39-win32.whl", hash = "sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a"}, - {file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"}, - {file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"}, + {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"}, + {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"}, + {file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa"}, + {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691"}, + {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825"}, + {file = "aiohttp-3.8.5-cp310-cp310-win32.whl", hash = "sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802"}, + {file = "aiohttp-3.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df"}, + {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9"}, + {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975"}, + {file = "aiohttp-3.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff"}, + {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5"}, + {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c"}, + {file = "aiohttp-3.8.5-cp311-cp311-win32.whl", hash = "sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945"}, + {file = "aiohttp-3.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755"}, + {file = "aiohttp-3.8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd"}, + {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af"}, + {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"}, + {file = "aiohttp-3.8.5-cp36-cp36m-win32.whl", hash = "sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e"}, + {file = "aiohttp-3.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6"}, + {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a"}, + {file = "aiohttp-3.8.5-cp37-cp37m-win32.whl", hash = "sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22"}, + {file = "aiohttp-3.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d"}, + {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced"}, + {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690"}, + {file = "aiohttp-3.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8"}, + {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b"}, + {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35"}, + {file = "aiohttp-3.8.5-cp38-cp38-win32.whl", hash = "sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c"}, + {file = "aiohttp-3.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e"}, + {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3"}, + {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51"}, + {file = "aiohttp-3.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1"}, + {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd"}, + {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91"}, + {file = "aiohttp-3.8.5-cp39-cp39-win32.whl", hash = "sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67"}, + {file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"}, + {file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"}, ] [package.dependencies] @@ -299,36 +299,33 @@ files = [ [[package]] name = "black" -version = "23.3.0" +version = "23.7.0" description = "The uncompromising code formatter." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, + {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, + {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, + {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, + {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, + {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, + {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, + {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, + {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, + {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, + {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, ] [package.dependencies] @@ -534,13 +531,13 @@ zstd = ["zstandard (==0.21.0)"] [[package]] name = "certifi" -version = "2023.5.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, - {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] @@ -621,97 +618,97 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.1.0" +version = "3.2.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, ] [[package]] name = "click" -version = "8.1.3" +version = "8.1.6" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, ] [package.dependencies] @@ -793,30 +790,34 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "cryptography" -version = "41.0.1" +version = "41.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699"}, - {file = "cryptography-41.0.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b"}, - {file = "cryptography-41.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3"}, - {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db"}, - {file = "cryptography-41.0.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31"}, - {file = "cryptography-41.0.1-cp37-abi3-win32.whl", hash = "sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5"}, - {file = "cryptography-41.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039"}, - {file = "cryptography-41.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a"}, - {file = "cryptography-41.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5"}, - {file = "cryptography-41.0.1.tar.gz", hash = "sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006"}, + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, + {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, + {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, + {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, + {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, + {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, + {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, + {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, + {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, + {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, ] [package.dependencies] @@ -856,13 +857,13 @@ files = [ [[package]] name = "docformatter" -version = "1.7.3" +version = "1.7.5" description = "Formats docstrings to follow PEP 257" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "docformatter-1.7.3-py3-none-any.whl", hash = "sha256:ba776f6305ff0ae9e1f42178975c0956ce01216e80b9d3e3e43daae9f742e321"}, - {file = "docformatter-1.7.3.tar.gz", hash = "sha256:f6ce59631d4ecc41af2780787b88f5dab94cdf1383796b1110318040c2f4ea36"}, + {file = "docformatter-1.7.5-py3-none-any.whl", hash = "sha256:a24f5545ed1f30af00d106f5d85dc2fce4959295687c24c8f39f5263afaf9186"}, + {file = "docformatter-1.7.5.tar.gz", hash = "sha256:ffed3da0daffa2e77f80ccba4f0e50bfa2755e1c10e130102571c890a61b246e"}, ] [package.dependencies] @@ -874,24 +875,24 @@ tomli = ["tomli (>=2.0.0,<3.0.0)"] [[package]] name = "docutils" -version = "0.19" +version = "0.20.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.7" files = [ - {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, - {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, + {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, + {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] [[package]] name = "elementpath" -version = "4.1.4" +version = "4.1.5" description = "XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and lxml" optional = false python-versions = ">=3.7" files = [ - {file = "elementpath-4.1.4-py3-none-any.whl", hash = "sha256:e7c6d25546dfb381a2c9cde3b78c0c40f52811e06eb810faf019e16c531a74bf"}, - {file = "elementpath-4.1.4.tar.gz", hash = "sha256:f991c42ff66fa06e219141ccf65890261e6635b448e7d4c2d8b62dc5bf1de9e8"}, + {file = "elementpath-4.1.5-py3-none-any.whl", hash = "sha256:2ac1a2fb31eb22bbbf817f8cf6752f844513216263f0e3892c8e79782fe4bb55"}, + {file = "elementpath-4.1.5.tar.gz", hash = "sha256:c2d6dc524b29ef751ecfc416b0627668119d8812441c555d7471da41d4bacb8d"}, ] [package.extras] @@ -1089,161 +1090,129 @@ email = ["email-validator"] [[package]] name = "frozenlist" -version = "1.3.3" +version = "1.4.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, - {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, - {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, - {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, - {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, - {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, - {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, - {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, - {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, - {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, - {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, - {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, - {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, - {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, - {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62"}, + {file = "frozenlist-1.4.0-cp310-cp310-win32.whl", hash = "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0"}, + {file = "frozenlist-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb"}, + {file = "frozenlist-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431"}, + {file = "frozenlist-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8"}, + {file = "frozenlist-1.4.0-cp38-cp38-win32.whl", hash = "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc"}, + {file = "frozenlist-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3"}, + {file = "frozenlist-1.4.0-cp39-cp39-win32.whl", hash = "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f"}, + {file = "frozenlist-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167"}, + {file = "frozenlist-1.4.0.tar.gz", hash = "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251"}, ] [[package]] name = "gevent" -version = "22.10.2" +version = "23.7.0" description = "Coroutine-based network library" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5" -files = [ - {file = "gevent-22.10.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:97cd42382421779f5d82ec5007199e8a84aa288114975429e4fd0a98f2290f10"}, - {file = "gevent-22.10.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1e1286a76f15b5e15f1e898731d50529e249529095a032453f2c101af3fde71c"}, - {file = "gevent-22.10.2-cp27-cp27m-win32.whl", hash = "sha256:59b47e81b399d49a5622f0f503c59f1ce57b7705306ea0196818951dfc2f36c8"}, - {file = "gevent-22.10.2-cp27-cp27m-win_amd64.whl", hash = "sha256:1d543c9407a1e4bca11a8932916988cfb16de00366de5bf7bc9e7a3f61e60b18"}, - {file = "gevent-22.10.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4e2f008c82dc54ec94f4de12ca6feea60e419babb48ec145456907ae61625aa4"}, - {file = "gevent-22.10.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:990d7069f14dc40674e0d5cb43c68fd3bad8337048613b9bb94a0c4180ffc176"}, - {file = "gevent-22.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f23d0997149a816a2a9045af29c66f67f405a221745b34cefeac5769ed451db8"}, - {file = "gevent-22.10.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b43d500d7d3c0e03070dee813335bb5315215aa1cf6a04c61093dfdd718640b3"}, - {file = "gevent-22.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b68f4c9e20e47ad49fe797f37f91d5bbeace8765ce2707f979a8d4ec197e4d"}, - {file = "gevent-22.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1f001cac0ba8da76abfeb392a3057f81fab3d67cc916c7df8ea977a44a2cc989"}, - {file = "gevent-22.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:3b7eae8a0653ba95a224faaddf629a913ace408edb67384d3117acf42d7dcf89"}, - {file = "gevent-22.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8f2477e7b0a903a01485c55bacf2089110e5f767014967ba4b287ff390ae2638"}, - {file = "gevent-22.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddaa3e310a8f1a45b5c42cf50b54c31003a3028e7d4e085059090ea0e7a5fddd"}, - {file = "gevent-22.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98bc510e80f45486ef5b806a1c305e0e89f0430688c14984b0dbdec03331f48b"}, - {file = "gevent-22.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877abdb3a669576b1d51ce6a49b7260b2a96f6b2424eb93287e779a3219d20ba"}, - {file = "gevent-22.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d21ad79cca234cdbfa249e727500b0ddcbc7adfff6614a96e6eaa49faca3e4f2"}, - {file = "gevent-22.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e955238f59b2947631c9782a713280dd75884e40e455313b5b6bbc20b92ff73"}, - {file = "gevent-22.10.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5aa99e4882a9e909b4756ee799c6fa0f79eb0542779fad4cc60efa23ec1b2aa8"}, - {file = "gevent-22.10.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:d82081656a5b9a94d37c718c8646c757e1617e389cdc533ea5e6a6f0b8b78545"}, - {file = "gevent-22.10.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f4bfd74c178351a4a05c5c7df6f8a0a279ff6f392b57608ce0e83c768207f9"}, - {file = "gevent-22.10.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ff3796692dff50fec2f381b9152438b221335f557c4f9b811f7ded51b7a25a1"}, - {file = "gevent-22.10.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f01c9adbcb605364694b11dcd0542ec468a29ac7aba2fb5665dc6caf17ba4d7e"}, - {file = "gevent-22.10.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9d85574eb729f981fea9a78998725a06292d90a3ed50ddca74530c3148c0be41"}, - {file = "gevent-22.10.2-cp36-cp36m-win32.whl", hash = "sha256:8c192d2073e558e241f0b592c1e2b34127a4481a5be240cad4796533b88b1a98"}, - {file = "gevent-22.10.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a2237451c721a0f874ef89dbb4af4fdc172b76a964befaa69deb15b8fff10f49"}, - {file = "gevent-22.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:53ee7f170ed42c7561fe8aff5d381dc9a4124694e70580d0c02fba6aafc0ea37"}, - {file = "gevent-22.10.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:96c56c280e3c43cfd075efd10b250350ed5ffd3c1514ec99a080b1b92d7c8374"}, - {file = "gevent-22.10.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c144e08dfad4106effc043a026e5d0c0eff6ad031904c70bf5090c63f3a6a7"}, - {file = "gevent-22.10.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:018f93de7d5318d2fb440f846839a4464738468c3476d5c9cf7da45bb71c18bd"}, - {file = "gevent-22.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7ed2346eb9dc4344f9cb0d7963ce5b74fe16fdd031a2809bb6c2b6eba7ebcd5"}, - {file = "gevent-22.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84c517e33ed604fa06b7d756dc0171169cc12f7fdd68eb7b17708a62eebf4516"}, - {file = "gevent-22.10.2-cp37-cp37m-win32.whl", hash = "sha256:4114f0f439f0b547bb6f1d474fee99ddb46736944ad2207cef3771828f6aa358"}, - {file = "gevent-22.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:0d581f22a5be6281b11ad6309b38b18f0638cf896931223cbaa5adb904826ef6"}, - {file = "gevent-22.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2929377c8ebfb6f4d868d161cd8de2ea6b9f6c7a5fcd4f78bcd537319c16190b"}, - {file = "gevent-22.10.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:efc003b6c1481165af61f0aeac248e0a9ac8d880bb3acbe469b448674b2d5281"}, - {file = "gevent-22.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db562a8519838bddad0c439a2b12246bab539dd50e299ea7ff3644274a33b6a5"}, - {file = "gevent-22.10.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1472012493ca1fac103f700d309cb6ef7964dcdb9c788d1768266e77712f5e49"}, - {file = "gevent-22.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c04ee32c11e9fcee47c1b431834878dc987a7a2cc4fe126ddcae3bad723ce89"}, - {file = "gevent-22.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8729129edef2637a8084258cb9ec4e4d5ca45d97ac77aa7a6ff19ccb530ab731"}, - {file = "gevent-22.10.2-cp38-cp38-win32.whl", hash = "sha256:ae90226074a6089371a95f20288431cd4b3f6b0b096856afd862e4ac9510cddd"}, - {file = "gevent-22.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:494c7f29e94df9a1c3157d67bb7edfa32a46eed786e04d9ee68d39f375e30001"}, - {file = "gevent-22.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:58898dbabb5b11e4d0192aae165ad286dc6742c543e1be9d30dc82753547c508"}, - {file = "gevent-22.10.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:4197d423e198265eef39a0dea286ef389da9148e070310f34455ecee8172c391"}, - {file = "gevent-22.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da4183f0b9d9a1e25e1758099220d32c51cc2c6340ee0dea3fd236b2b37598e4"}, - {file = "gevent-22.10.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5488eba6a568b4d23c072113da4fc0feb1b5f5ede7381656dc913e0d82204e2"}, - {file = "gevent-22.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319d8b1699b7b8134de66d656cd739b308ab9c45ace14d60ae44de7775b456c9"}, - {file = "gevent-22.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f3329bedbba4d3146ae58c667e0f9ac1e6f1e1e6340c7593976cdc60aa7d1a47"}, - {file = "gevent-22.10.2-cp39-cp39-win32.whl", hash = "sha256:172caa66273315f283e90a315921902cb6549762bdcb0587fd60cb712a9d6263"}, - {file = "gevent-22.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:323b207b281ba0405fea042067fa1a61662e5ac0d574ede4ebbda03efd20c350"}, - {file = "gevent-22.10.2-pp27-pypy_73-win_amd64.whl", hash = "sha256:ed7f16613eebf892a6a744d7a4a8f345bc6f066a0ff3b413e2479f9c0a180193"}, - {file = "gevent-22.10.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a47a4e77e2bc668856aad92a0b8de7ee10768258d93cd03968e6c7ba2e832f76"}, - {file = "gevent-22.10.2.tar.gz", hash = "sha256:1ca01da176ee37b3527a2702f7d40dbc9ffb8cfc7be5a03bfa4f9eec45e55c46"}, +python-versions = ">=3.8" +files = [ + {file = "gevent-23.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:add904a7ef960cd4e133e61eb7413982c5e4203928160be1c09752ac06a25e71"}, + {file = "gevent-23.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bd9ea1b5fbdc7e5921a9e515f34a450eb3927a902253a33caedcce2d19d7d96"}, + {file = "gevent-23.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c7c349aa23d67cf5cc3b2c87aaedcfead976d0577b1cfcd07ffeba63baba79c"}, + {file = "gevent-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92b837b60e850c50fc6d723d1e363e786d37fd9d51e564e07df52ad5e8a86d4"}, + {file = "gevent-23.7.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6a51a8e3cdaa6901e47d56f84cb5f92b1bf3deea920bce69cf7a245df16159ac"}, + {file = "gevent-23.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1dba07207b15b371e50372369edf256a142cb5cdf8599849cbf8660327efa06"}, + {file = "gevent-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:34086bcc1252ae41e1cb81cf13c4a7678031595c12f4e9a1c3d0ab433f20826a"}, + {file = "gevent-23.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5da07d65dfa23fe419c37cea110bf951b42af6bf3a1fff244043a75c9185dbd5"}, + {file = "gevent-23.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4d7be3352126458cc818309ca6a3b678c209b1ae33e56b6975c6a8309f2068"}, + {file = "gevent-23.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76ca6f893953ab898ebbff5d772103318a85044e55d0bad401d6b49d71bb76e7"}, + {file = "gevent-23.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aeb1511cf0786152af741c47ee462dac81b57bbd1fbbe08ab562b6c8c9ad75ed"}, + {file = "gevent-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:919423e803939726c99ab2d29ea46b8676af549cee72d263f2b24758ec607b2c"}, + {file = "gevent-23.7.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cea93f4f77badbddc711620cca164ad75c74056603908e621a5ba1b97adbc39c"}, + {file = "gevent-23.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dec7b08daf08385fb281b81ec2e7e703243975d867f40ae0a8a3e30b380eb9ea"}, + {file = "gevent-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:f522b6b015f1bfa9d8d3716ddffb23e3d4a8933df3e4ebf0a29a65a9fa74382b"}, + {file = "gevent-23.7.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:746a1e12f280dab07389e6709164b1e1a6caaf50493ea5b1dcaa73cff005174c"}, + {file = "gevent-23.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b230007a665d2cf5cf8878c9f56a2b8bacbdc4fe0235afc5269b71cd00528e5"}, + {file = "gevent-23.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1d2f1e67d04fde47ca2deac89733df28ef3a7ec1d7359a79f57d4778cced16d"}, + {file = "gevent-23.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:debc177e88a8c876cb1a4d974f985d03670177bdc61e1c084a8d525f1a50b12d"}, + {file = "gevent-23.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b3dd449c80814357f6568eb095a2be2421b805d59fa97c65094707e04a181f9"}, + {file = "gevent-23.7.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:769e8811ded08fe7d8b09ad8ebb72d47aecc112411e0726e7296b7ed187ed629"}, + {file = "gevent-23.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11b9bb0bce45170ff992760385a86e6955ccb88dba4a82a64d5ce9459290d8d6"}, + {file = "gevent-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e0d76a7848726e0646324a1adc011355dcd91875e7913badd1ada2e5eeb8a6e"}, + {file = "gevent-23.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a226b42cb9a49580ca7729572a4f8289d1fa28cd2529c9f4eed3e14b995d1c9c"}, + {file = "gevent-23.7.0-cp38-cp38-win32.whl", hash = "sha256:1234849b0bc4df560924aa92f7c01ca3f310677735fb508a2b0d7a61bb946916"}, + {file = "gevent-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:a8f62e8d37913512823923e05607a296389aeb50ccca8a271ae7cedb5b17faeb"}, + {file = "gevent-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369241d1a6a3fe3ef4eba454b71e0168026560c5344fc4bc37196867041982ac"}, + {file = "gevent-23.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:94b013587f7c4697d620c129627f7b12d7d9f6e40ab198635891ca2098cd8556"}, + {file = "gevent-23.7.0-cp39-cp39-win32.whl", hash = "sha256:83b6d61a8e9da25edb304ca7fba19ee57bb1ffa801f9df3e668bfed7bb8386cb"}, + {file = "gevent-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:8c284390f0f6d0b5be3bf805fa8e0ae1329065f2b0ac5af5423c67183197deb8"}, + {file = "gevent-23.7.0.tar.gz", hash = "sha256:d0d3630674c1b344b256a298ab1ff43220f840b12af768131b5d74e485924237"}, ] [package.dependencies] cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} -greenlet = {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\""} -setuptools = "*" +greenlet = [ + {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.12\""}, + {version = ">=3.0a1", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.12\""}, +] "zope.event" = "*" "zope.interface" = "*" [package.extras] dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] -docs = ["repoze.sphinx.autointerface", "sphinxcontrib-programoutput", "zope.schema"] +docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] monitor = ["psutil (>=5.7.0)"] -recommended = ["backports.socketpair", "cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)", "selectors2"] -test = ["backports.socketpair", "cffi (>=1.12.2)", "contextvars (==2.4)", "coverage (>=5.0)", "coveralls (>=1.7.0)", "dnspython (>=1.16.0,<2.0)", "futures", "idna", "mock", "objgraph", "psutil (>=5.7.0)", "requests", "selectors2"] +recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] +test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"] [[package]] name = "greenlet" @@ -1318,19 +1287,91 @@ files = [ docs = ["Sphinx", "docutils (<0.18)"] test = ["objgraph", "psutil"] +[[package]] +name = "greenlet" +version = "3.0.0a1" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.0a1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8dd92fd76a61af2abc8ccad0c6c6069b3c4ebd4727ecc9a7c33aae37651c8c7"}, + {file = "greenlet-3.0.0a1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:889934aa8d72b6bfc46babd1dc4b817a56c97ec0f4a10ae7551fb60ab1f96fae"}, + {file = "greenlet-3.0.0a1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b767930af686551dc96a5eb70af3736709d547ffa275c11a5e820bfb3ae61d8d"}, + {file = "greenlet-3.0.0a1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a1f4d256b81f59ba87bb7a29b9b38b1c018e052dba60a543cb0ddb5062d159"}, + {file = "greenlet-3.0.0a1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3fb459ced6c5e3b2a895f23f1400f93e9b24d85c30fbe2d637d4f7706a1116b"}, + {file = "greenlet-3.0.0a1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180ec55cb127bc745669eddc9793ffab6e0cf7311e67e1592f183d6ca00d88c1"}, + {file = "greenlet-3.0.0a1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab81f9ff3e3c2ca65e824454214c10985a846cd9bee5f4d04e15cd875d9fe13b"}, + {file = "greenlet-3.0.0a1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:21ebcb570e0d8501457d6a2695a44c5af3b6c2143dc6644ec73574beba067c90"}, + {file = "greenlet-3.0.0a1-cp310-cp310-win_amd64.whl", hash = "sha256:4d0c0ffd732466ff324ced144fad55ed5deca36f6036c1d8f04cec69b084c9d6"}, + {file = "greenlet-3.0.0a1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4a2d6ed0515c05afd5cc435361ced0baabd9ba4536ddfe8ad9a95bcb702c8ce"}, + {file = "greenlet-3.0.0a1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffb9f8969789771e95d3c982a36be81f0adfaa7302a1d56e29f168ca15e284b8"}, + {file = "greenlet-3.0.0a1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3f3568478bc21b85968e8038c4f98f4bf0039a692791bc324b5e0d1522f4b1"}, + {file = "greenlet-3.0.0a1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e160a65cc6023a237be870f2072513747d512a1d018efa083acce0b673cccc0"}, + {file = "greenlet-3.0.0a1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e31d1a33dc9006b278f72cb0aacfe397606c2693aa2fdc0c2f2dcddbad9e0b53"}, + {file = "greenlet-3.0.0a1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00550757fca1b9cbc479f8eb1cf3514dbc0103b3f76eae46341c26ddcca67a9"}, + {file = "greenlet-3.0.0a1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2840187a94e258445e62ff1545e34f0b1a14aef4d0078e5c88246688d2b6515e"}, + {file = "greenlet-3.0.0a1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:271ed380389d2f7e4c1545b6e0837986e62504ab561edbaff05da9c9f3f98f96"}, + {file = "greenlet-3.0.0a1-cp311-cp311-win_amd64.whl", hash = "sha256:4ff2a765f4861fc018827eab4df1992f7508d06c62de5d2fe8a6ac2233d4f1d0"}, + {file = "greenlet-3.0.0a1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:463d63ca5d8c236788284a9a44b9715372a64d5318a6b5eee36815df1ea0ba3d"}, + {file = "greenlet-3.0.0a1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3530c0ec1fc98c43d5b7061781a8c55bd0db44f789f8152e19d9526cbed6021"}, + {file = "greenlet-3.0.0a1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bce5cf2b0f0b29680396c5c98ab39a011bd70f2dfa8b8a6811a69ee6d920cf9f"}, + {file = "greenlet-3.0.0a1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5672082576d0e9f52fa0fa732ff57254d65faeb4a471bc339fe54b58b3e79d2"}, + {file = "greenlet-3.0.0a1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5552d7be37d878e9b6359bbffa0512d857bb9703616a4c0656b49c10739d5971"}, + {file = "greenlet-3.0.0a1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:36cebce1f30964d5672fd956860e7e7b69772da69658d5743cb676b442eeff36"}, + {file = "greenlet-3.0.0a1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:665942d3a954c3e4c976581715f57fb3b86f4cf6bae3ac30b133f8ff777ac6c7"}, + {file = "greenlet-3.0.0a1-cp312-cp312-win_amd64.whl", hash = "sha256:ce70aa089ec589b5d5fab388af9f8c9f9dfe8fe4ad844820a92eb240d8628ddf"}, + {file = "greenlet-3.0.0a1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:17503397bf6cbb5e364217143b6150c540020c51a3f6b08f9a20cd67c25e2ca8"}, + {file = "greenlet-3.0.0a1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d61bad421c1f496f9fb6114dbd7c30a1dac0e9ff90e9be06f4472cbd8f7a1704"}, + {file = "greenlet-3.0.0a1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bab71f73001cd15723c4e2ca398f2f48e0a3f584c619eefddb1525e8986e06eb"}, + {file = "greenlet-3.0.0a1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f61df4fe07864561f49b45c8bd4d2c42e3f03d2872ed05c844902a58b875028"}, + {file = "greenlet-3.0.0a1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c02e514c72e745e49a3ae7e672a1018ba9b68460c21e0361054e956e5d595bc6"}, + {file = "greenlet-3.0.0a1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd31ab223e43ac64fd23f8f5dad249addadac2a459f040546200acbf7e84e353"}, + {file = "greenlet-3.0.0a1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6aac94ff957b5dea0216af71ab59c602e1b947b394e4f5e878a5a65643090038"}, + {file = "greenlet-3.0.0a1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d7ba2e5cb119eddbc10874b41047ad99525e39e397f7aef500e6da0d6f46ab91"}, + {file = "greenlet-3.0.0a1-cp37-cp37m-win32.whl", hash = "sha256:ac10196b8cde7a082e4e371ff171407270d3337c8d57ed43030094eb01d9c95c"}, + {file = "greenlet-3.0.0a1-cp37-cp37m-win_amd64.whl", hash = "sha256:0a9dfcadc1d79696e90ccb1275c30ad4ec5fd3d1ab3ae6671286fac78ef33435"}, + {file = "greenlet-3.0.0a1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5942b1d6ba447cff1ec23a21ec525dde2288f00464950bc647f4e0f03bd537d1"}, + {file = "greenlet-3.0.0a1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:450a7e52a515402fd110ba807f1a7d464424bfa703be4effbcb97e1dfbfcc621"}, + {file = "greenlet-3.0.0a1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:df34b52aa50a38d7a79f3abc9fda7e400791447aa0400ed895f275f6d8b0bb1f"}, + {file = "greenlet-3.0.0a1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cda110faee67613fed221f90467003f477088ef1cc84c8fc88537785a5b4de9"}, + {file = "greenlet-3.0.0a1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f313771cb8ee0a04dfdf586b7d4076180d80c94be09049daeea018089b5b957"}, + {file = "greenlet-3.0.0a1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42bfe67824a9b53e73f568f982f0d1d4c7ac0f587d2e702a23f8a7b505d7b7c2"}, + {file = "greenlet-3.0.0a1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0fc20e6e6b298861035a5fc5dcf9fbaa0546318e8bda81112591861a7dcc28f"}, + {file = "greenlet-3.0.0a1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f34ec09702be907727fd479046193725441aaaf7ed4636ca042734f469bb7451"}, + {file = "greenlet-3.0.0a1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:270432cfdd6a50016b8259b3bbf398a3f7c06a06f2c68c7b93e49f53bc193bcf"}, + {file = "greenlet-3.0.0a1-cp38-cp38-win32.whl", hash = "sha256:d47b2e1ad1429da9aa459ef189fbcd8a74ec28a16bc4c3f5f3cf3f88e36535eb"}, + {file = "greenlet-3.0.0a1-cp38-cp38-win_amd64.whl", hash = "sha256:e7b192c3df761d0fdd17c2d42d41c28460f124f5922e8bd524018f1d35610682"}, + {file = "greenlet-3.0.0a1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e20d5e8dc76b73db9280464d6e81bea05e51a99f4d4dd29c5f78dc79f294a5d3"}, + {file = "greenlet-3.0.0a1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:ba94c08321b5d345100fc64eb1ab235f42faf9aabba805cface55ebe677f1c2c"}, + {file = "greenlet-3.0.0a1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:24071eee113d75fedebaeb86264d94f04b5a24e311c5ba3e8003c07d00112a7e"}, + {file = "greenlet-3.0.0a1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:585810056a8adacd3152945ebfcd25deb58335d41f16ae4e0f3d768918957f9a"}, + {file = "greenlet-3.0.0a1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3a99f890f2cc5535e1b3a90049c6ca9ff9da9ec251cc130c8d269997f9d32ee"}, + {file = "greenlet-3.0.0a1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c355c99be5bb23e85d899b059a4f22fdf8a0741c57e7029425ee63eb436f689"}, + {file = "greenlet-3.0.0a1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dde0ab052c7a1deee8d13d72c37f2afecee30ebdf6eb139790157eaddf04dd61"}, + {file = "greenlet-3.0.0a1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ed0f4fad4c3656e34d20323a789b6a2d210a6bb82647d9c86dded372f55c58a1"}, + {file = "greenlet-3.0.0a1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53abf19b7dc62795c67b8d0a3d8ef866db166b21017632fff2624cf8fbf3481c"}, + {file = "greenlet-3.0.0a1-cp39-cp39-win32.whl", hash = "sha256:2fcf7af83516db35af3d0ed5d182dea8585eddd891977adff1b74212f4bfd2fd"}, + {file = "greenlet-3.0.0a1-cp39-cp39-win_amd64.whl", hash = "sha256:68368e908f14887fb202a81960bfbe3a02d97e6d3fa62b821556463084ffb131"}, + {file = "greenlet-3.0.0a1.tar.gz", hash = "sha256:1bd4ea36f0aeb14ca335e0c9594a5aaefa1ac4e2db7d86ba38f0be96166b3102"}, +] + +[package.extras] +docs = ["Sphinx"] +test = ["objgraph", "psutil"] + [[package]] name = "gunicorn" -version = "20.1.0" +version = "21.2.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.5" files = [ - {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, - {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, + {file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"}, + {file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"}, ] [package.dependencies] -setuptools = ">=3.0" +packaging = "*" [package.extras] eventlet = ["eventlet (>=0.24.1)"] @@ -1551,95 +1592,110 @@ files = [ [[package]] name = "lxml" -version = "4.9.2" +version = "4.9.3" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ - {file = "lxml-4.9.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2"}, - {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892"}, - {file = "lxml-4.9.2-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a"}, - {file = "lxml-4.9.2-cp27-cp27m-win32.whl", hash = "sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de"}, - {file = "lxml-4.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3"}, - {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50"}, - {file = "lxml-4.9.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975"}, - {file = "lxml-4.9.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c"}, - {file = "lxml-4.9.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a"}, - {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4"}, - {file = "lxml-4.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4"}, - {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7"}, - {file = "lxml-4.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184"}, - {file = "lxml-4.9.2-cp310-cp310-win32.whl", hash = "sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda"}, - {file = "lxml-4.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab"}, - {file = "lxml-4.9.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9"}, - {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf"}, - {file = "lxml-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380"}, - {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92"}, - {file = "lxml-4.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1"}, - {file = "lxml-4.9.2-cp311-cp311-win32.whl", hash = "sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33"}, - {file = "lxml-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd"}, - {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0"}, - {file = "lxml-4.9.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e"}, - {file = "lxml-4.9.2-cp35-cp35m-win32.whl", hash = "sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df"}, - {file = "lxml-4.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5"}, - {file = "lxml-4.9.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53"}, - {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7"}, - {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe"}, - {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"}, - {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1"}, - {file = "lxml-4.9.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e"}, - {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74"}, - {file = "lxml-4.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38"}, - {file = "lxml-4.9.2-cp36-cp36m-win32.whl", hash = "sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5"}, - {file = "lxml-4.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3"}, - {file = "lxml-4.9.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03"}, - {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941"}, - {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726"}, - {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b"}, - {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894"}, - {file = "lxml-4.9.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45"}, - {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e"}, - {file = "lxml-4.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b"}, - {file = "lxml-4.9.2-cp37-cp37m-win32.whl", hash = "sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe"}, - {file = "lxml-4.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9"}, - {file = "lxml-4.9.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8"}, - {file = "lxml-4.9.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24"}, - {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889"}, - {file = "lxml-4.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f"}, - {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03"}, - {file = "lxml-4.9.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c"}, - {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f"}, - {file = "lxml-4.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457"}, - {file = "lxml-4.9.2-cp38-cp38-win32.whl", hash = "sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b"}, - {file = "lxml-4.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7"}, - {file = "lxml-4.9.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1"}, - {file = "lxml-4.9.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140"}, - {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4"}, - {file = "lxml-4.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf"}, - {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947"}, - {file = "lxml-4.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5"}, - {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5"}, - {file = "lxml-4.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2"}, - {file = "lxml-4.9.2-cp39-cp39-win32.whl", hash = "sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1"}, - {file = "lxml-4.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f"}, - {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c"}, - {file = "lxml-4.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a"}, - {file = "lxml-4.9.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419"}, - {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05"}, - {file = "lxml-4.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f"}, - {file = "lxml-4.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9"}, - {file = "lxml-4.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5"}, - {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746"}, - {file = "lxml-4.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7"}, - {file = "lxml-4.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409"}, - {file = "lxml-4.9.2.tar.gz", hash = "sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67"}, + {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"}, + {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"}, + {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"}, + {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"}, + {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"}, + {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, + {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, + {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, + {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, + {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, + {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, + {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, + {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, + {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, + {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, + {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, + {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, + {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, + {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"}, + {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"}, + {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"}, + {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"}, + {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"}, + {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"}, + {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"}, + {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"}, + {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, + {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, + {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, + {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, + {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, + {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, + {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, + {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, + {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, + {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, + {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, + {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, + {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, + {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, + {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, + {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, + {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, + {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, + {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=0.29.7)"] +source = ["Cython (>=0.29.35)"] [[package]] name = "mailmanclient" @@ -1817,22 +1873,22 @@ files = [ [[package]] name = "marshmallow" -version = "3.19.0" +version = "3.20.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "marshmallow-3.19.0-py3-none-any.whl", hash = "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b"}, - {file = "marshmallow-3.19.0.tar.gz", hash = "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78"}, + {file = "marshmallow-3.20.1-py3-none-any.whl", hash = "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c"}, + {file = "marshmallow-3.20.1.tar.gz", hash = "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889"}, ] [package.dependencies] packaging = ">=17.0" [package.extras] -dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"] -docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.3.0)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] -lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)"] +dev = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.13)", "autodocsumm (==0.2.11)", "sphinx (==7.0.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -1865,75 +1921,75 @@ marshmallow = ">=3.0.0" [[package]] name = "mmh3" -version = "4.0.0" +version = "4.0.1" description = "Python extension for MurmurHash (MurmurHash3), a set of fast and robust hash functions." optional = false python-versions = "*" files = [ - {file = "mmh3-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:214ae1ab976401b3fd8da2a3828d1520d31592efacab63f90c222a0e69ad68cf"}, - {file = "mmh3-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ab345fba03d94cd08494a60c244085fb800645881639795b9390900672ee1c4"}, - {file = "mmh3-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:072d449ed3769b7faff5ce7fe05323d2602834f03dfc3969dcedb183b1d902ec"}, - {file = "mmh3-4.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e78c400595fb10c5ba46bd2386a0f1b2d5345c09d391882f96a27b4cf8bfc84"}, - {file = "mmh3-4.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec5c0e0f4be15d73d2ff8d00212c67fcd4f36bca4b43e3c940c545341805dd22"}, - {file = "mmh3-4.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:006ac0d3fe9cbc2855f63491ac3ce77290cedb6adf147d9bf803eb4097f765a5"}, - {file = "mmh3-4.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dceabcf98a3c5c9a137a81c7e2f8cb9b438c1b1da6d8c4d7ec8ca9df52cdd3c"}, - {file = "mmh3-4.0.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba8734f408a08822b2597a70a58549fd84a7afd86a18839ae1bb0bc9b9d2b816"}, - {file = "mmh3-4.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2c2642101105a36db0f936f137cd27bdc75335739e06d0d172d1ece907cb99e"}, - {file = "mmh3-4.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7754d661e8dd855b7950cb94f33db45ee53f12bc271eb376d5a8c84c4c039ad4"}, - {file = "mmh3-4.0.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c73a503ab4297b4a2125eeda50b28bcd7eb8a6eef7b52e2a4a4c810267d0076"}, - {file = "mmh3-4.0.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6dd94d8650576b1395640dfa62f49aa5bdead7200a655efee215b4a9c78f4882"}, - {file = "mmh3-4.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:496e39cc349630294fbf39db7571d5833e9e779df90fc0cc1f652c2f1df7b1c1"}, - {file = "mmh3-4.0.0-cp310-cp310-win32.whl", hash = "sha256:4333668e3ddadd1795c223b44376f18b22fec205fd9604ae7b8e7ab3cdf12ce0"}, - {file = "mmh3-4.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:c6af081268d05a645c9195968baf901d1a70f64e05586c9393f744ce0943a6d8"}, - {file = "mmh3-4.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:14e835ea27fe36bf9fe584dbd2063c9c3c68e3bb3d11c41c5922bf5b9759b2e0"}, - {file = "mmh3-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dfbd7db8ed8ce8ab0cb1dee1a25f15a900c9e66ef12004ab593984a51ba36fae"}, - {file = "mmh3-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e39f5aa5ce2cda81f1f5856092bdb8e9263cb021990c1aede92c31c84a593ef"}, - {file = "mmh3-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:646eb838998f60eea6fbcfde8ddef1081c4a10a30c09400ee2ec57a789cafb9b"}, - {file = "mmh3-4.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b895044e24b845e75af3cb6ec3cd7ae88fa87ffcba93f2bfed3ae0c226d4a2e5"}, - {file = "mmh3-4.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd912d31aa5e2327342777f8b9cde8b82adc3525fd7fa535a2b7b76789f4162a"}, - {file = "mmh3-4.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a0323dfd7609634867e5fb23cf21f96c36792b558d88af73fdbc97e3440ea79"}, - {file = "mmh3-4.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2107a8de64e79d7b532ae9844e8ce79ff3284d5b6661b0566e5511cf11c6efca"}, - {file = "mmh3-4.0.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c6e9e87c4ee08b60c5b5f967ac82777974b2aa0df5ef6f574c66a775231470"}, - {file = "mmh3-4.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:54064f72545c187150edbbf2e6c5c39017797d851f99aee3726ce5dc26d786f2"}, - {file = "mmh3-4.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1a9e6244923549e23d4b88c36d144d326092e0d33b802295d04c76e5dde567f4"}, - {file = "mmh3-4.0.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:087ee4a8c6dbfb4f356aaf32879846ad13e92894201f4d221a087fbb8857ec9a"}, - {file = "mmh3-4.0.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e6e5e4f543bf798161321deb186723a4ef530e7f216ea4c153e525f63c78fc78"}, - {file = "mmh3-4.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:df22bd073107fe20006f9197ba29579e915a9a89142ef65130939aa0136c78e7"}, - {file = "mmh3-4.0.0-cp311-cp311-win32.whl", hash = "sha256:f4ec497d5926842a45f235d1eda03ef2cc10e4ccb0738bbe828837817f0fbbfd"}, - {file = "mmh3-4.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a32c815f4d32bcbf71147b9109904e34f33716bed344955d13983026dc8568e0"}, - {file = "mmh3-4.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:72706beb124293b58968881ce3d59f678538c4dec3977c4edd9a9db402ad4486"}, - {file = "mmh3-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:86a8f5af47b78fd5e70e4d48c6da0a04492ab267780ff87e62ddfae5260c3443"}, - {file = "mmh3-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:285751d7e9a98ea842186240ad7a1ca33f0ffa6c0c7e6e3511ac094f8b4b289d"}, - {file = "mmh3-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8a334ce72e7b1602a951ae24453c087f0729049281954f57c2bbc3a3eddee4bb"}, - {file = "mmh3-4.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5638d5d26f7bf1bd79233e5140303ac47c506d51d86f67595ee851f2d49fa480"}, - {file = "mmh3-4.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1755485dc411336c429824258f2fe1a2f53fad2ba1481e922111312d0a698cf4"}, - {file = "mmh3-4.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a7f6089c37b83c5209fec02492418222e0051f09f27bc7fce9d49ee36603cd"}, - {file = "mmh3-4.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5cf444ce11571d0cf65516255b119d3dd733f86120e0acf7371363c97e396d"}, - {file = "mmh3-4.0.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ca5f2ad2af9decee4f9a3c91a823bf71c4f634ff400b829c648c0dc5cd7503f"}, - {file = "mmh3-4.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4c4d482434edf2581ae6bdaf99840791938b089acef56832d13f91e81745bff1"}, - {file = "mmh3-4.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a14c10d50526ee91ca474780f029b97d78fd9bb47ae9c248b8cb1a964525f8f"}, - {file = "mmh3-4.0.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e08ebbdfa008f5d957e69031ab625f7d22313fdff14f845d213ab06f585846e4"}, - {file = "mmh3-4.0.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:2cc0e1872acff360e95f8e3768b5f1777a1ebc700caf88a8a22adb153d24b9fa"}, - {file = "mmh3-4.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2070b169a8b82ad4dcbc6c34fcfd538428322e1f2db5dce349023aad2ba0dc94"}, - {file = "mmh3-4.0.0-cp38-cp38-win32.whl", hash = "sha256:e390c820a31146c73b0cb60da65906a866d4fe43b4688cc3cdf5f33c423d09a1"}, - {file = "mmh3-4.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:55ac731333b5e0e9f93ad56cab95b05ef1621a5a87568e36ec17dc9dd761abbe"}, - {file = "mmh3-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9f069b5421b6d2ac24dd1928788f986cb5393baeca8d758838946e3546a1a723"}, - {file = "mmh3-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:363a9a4342aa93d524b01336a646da4b20fb52b22b883d7a4a173f4b4e015451"}, - {file = "mmh3-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:453fff6505db5f3c2e2ee562744b8de49a58f83a609f178a1d3238036fbb4a72"}, - {file = "mmh3-4.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8397bb38314ba93f9c87e7feb724d093a3b3df566dccab9d088e18792c8ad6fd"}, - {file = "mmh3-4.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:130801c630768b39c0c115b4df1ba649318179a47981cbdd7171c8f78cfe1e4b"}, - {file = "mmh3-4.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ad9a741871550ecb7217449e423806f8e6e76c6fe90383992c8db15cb55e9d9"}, - {file = "mmh3-4.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07a7447cbfcaa269d8f8c64dd12924fe523f0ac04135428a1f327d769fa9b1a4"}, - {file = "mmh3-4.0.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95d03f341ce11ee9f325af910bc588dda519f4b15b60abab72ea286a6aad572b"}, - {file = "mmh3-4.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2cff9758d6b0f7f00d805e9d1d019e8b7f4da4b56795843e0339633fd67bc3a4"}, - {file = "mmh3-4.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:28c182fcb472bb102bb6f8f92bcc268be469220e8ba6dca383860e494a91671f"}, - {file = "mmh3-4.0.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:b52775bffc0463e32cddaea76afd61a992b15091fe1955418b44cb7b87a7aea9"}, - {file = "mmh3-4.0.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ec8ba7a8357a33af6c41a29a3015f796643e998a1a33d95cb773efc98ae669d2"}, - {file = "mmh3-4.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:129f2c948ce8b884ddcad0f755a8b3245302eb06c3de1b4aa9a45b643c63ca04"}, - {file = "mmh3-4.0.0-cp39-cp39-win32.whl", hash = "sha256:a373025a487295c9e6cbf86159665f49963077d39a843707519c678f7e6791a2"}, - {file = "mmh3-4.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:5790d3bf330a6be8bea19fcc03965a5cf6c9a37021ecf9202e54ed7c3d33bd23"}, - {file = "mmh3-4.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:2a2db279f0c97619a6d3cc291168cc47a3cbd73cf1767a058e5cc765252260a1"}, - {file = "mmh3-4.0.0.tar.gz", hash = "sha256:056b83d04e595547d0407cc8e5aa5d8ba8802a8afa417b64c1c30235b5389e30"}, + {file = "mmh3-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b719ba87232749095011d567a36a25e40ed029fc61c47e74a12416d8bb60b311"}, + {file = "mmh3-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f0ad423711c5096cf4a346011f3b3ec763208e4f4cc4b10ed41cad2a03dbfaed"}, + {file = "mmh3-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80918e3f8ab6b717af0a388c14ffac5a89c15d827ff008c1ef545b8b32724116"}, + {file = "mmh3-4.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8222cd5f147defa1355b4042d590c34cef9b2bb173a159fcb72cda204061a4ac"}, + {file = "mmh3-4.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3821bcd1961ef19247c78c5d01b5a759de82ab0c023e2ff1d5ceed74322fa018"}, + {file = "mmh3-4.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59f7ed28c24249a54665f1ed3f6c7c1c56618473381080f79bcc0bd1d1db2e4a"}, + {file = "mmh3-4.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dacd8d07d4b9be8f0cb6e8fd9a08fc237c18578cf8d42370ee8af2f5a2bf1967"}, + {file = "mmh3-4.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd00883ef6bcf7831026ce42e773a4b2a4f3a7bf9003a4e781fecb1144b06c1"}, + {file = "mmh3-4.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:df73d1c7f0c50c0f8061cd349968fd9dcc6a9e7592d1c834fa898f9c98f8dd7e"}, + {file = "mmh3-4.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f41eeae98f15af0a4ba2a92bce11d8505b612012af664a7634bbfdba7096f5fc"}, + {file = "mmh3-4.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ce9bb622e9f1162cafd033071b32ac495c5e8d5863fca2a5144c092a0f129a5b"}, + {file = "mmh3-4.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:dd92e0ff9edee6af960d9862a3e519d651e6344321fd280fb082654fc96ecc4d"}, + {file = "mmh3-4.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aefa8ac8c8fc8ad93365477baef2125dbfd7235880a9c47dca2c46a0af49ef7"}, + {file = "mmh3-4.0.1-cp310-cp310-win32.whl", hash = "sha256:a076ea30ec279a63f44f4c203e4547b5710d00581165fed12583d2017139468d"}, + {file = "mmh3-4.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5aa1e87e448ee1ffa3737b72f2fe3f5960159ab75bbac2f49dca6fb9797132f6"}, + {file = "mmh3-4.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:45155ff2f291c3a1503d1c93e539ab025a13fd8b3f2868650140702b8bd7bfc2"}, + {file = "mmh3-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:91f81d6dd4d0c3b4235b4a58a545493c946669c751a2e0f15084171dc2d81fee"}, + {file = "mmh3-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbfddaf55207798f5b29341e5b3a24dbff91711c51b1665eabc9d910255a78f0"}, + {file = "mmh3-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0deb8e19121c0896fdc709209aceda30a367cda47f4a884fcbe56223dbf9e867"}, + {file = "mmh3-4.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df468ac7b61ec7251d7499e27102899ca39d87686f659baf47f84323f8f4541f"}, + {file = "mmh3-4.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84936c113814c6ef3bc4bd3d54f538d7ba312d1d0c2441ac35fdd7d5221c60f6"}, + {file = "mmh3-4.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b1df3cf5ce5786aa093f45462118d87ff485f0d69699cdc34f6289b1e833632"}, + {file = "mmh3-4.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da281aa740aa9e7f9bebb879c1de0ea9366687ece5930f9f5027e7c87d018153"}, + {file = "mmh3-4.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ec380933a56eb9fea16d7fcd49f1b5a5c92d7d2b86f25e9a845b72758ee8c42"}, + {file = "mmh3-4.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2fa905fcec8a30e1c0ef522afae1d6170c4f08e6a88010a582f67c59209fb7c7"}, + {file = "mmh3-4.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9b23a06315a65ef0b78da0be32409cfce0d6d83e51d70dcebd3302a61e4d34ce"}, + {file = "mmh3-4.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:36c27089b12026db14be594d750f7ea6d5d785713b40a971b063f033f5354a74"}, + {file = "mmh3-4.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:6338341ae6fa5eaa46f69ed9ac3e34e8eecad187b211a6e552e0d8128c568eb1"}, + {file = "mmh3-4.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1aece29e27d0c8fb489d00bb712fba18b4dd10e39c9aec2e216c779ae6400b8f"}, + {file = "mmh3-4.0.1-cp311-cp311-win32.whl", hash = "sha256:2733e2160c142eed359e25e5529915964a693f0d043165b53933f904a731c1b3"}, + {file = "mmh3-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:09f9f643e0b7f8d98473efdfcdb155105824a38a1ada374625b84c1208197a9b"}, + {file = "mmh3-4.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:d93422f38bc9c4d808c5438a011b769935a87df92ce277e9e22b6ec0ae8ed2e2"}, + {file = "mmh3-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:41013c033dc446d3bfb573621b8b53223adcfcf07be1da0bcbe166d930276882"}, + {file = "mmh3-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be46540eac024dd8d9b82899d35b2f23592d3d3850845aba6f10e6127d93246b"}, + {file = "mmh3-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0e64114b30c6c1e30f8201433b5fa6108a74a5d6f1a14af1b041360c0dd056aa"}, + {file = "mmh3-4.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:275637ecca755565e3b0505d3ecf8e1e0a51eb6a3cbe6e212ed40943f92f98cd"}, + {file = "mmh3-4.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:955178c8e8d3bc9ad18eab443af670cd13fe18a6b2dba16db2a2a0632be8a133"}, + {file = "mmh3-4.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:750afe0477e0c17904611045ad311ff10bc6c2ec5f5ddc5dd949a2b9bf71d5d5"}, + {file = "mmh3-4.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b7c18c35e9d6a59d6c5f94a6576f800ff2b500e41cd152ecfc7bb4330f32ba2"}, + {file = "mmh3-4.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b8635b1fc6b25d93458472c5d682a1a4b9e6c53e7f4ca75d2bf2a18fa9363ae"}, + {file = "mmh3-4.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:057b8de47adee8ad0f2e194ffa445b9845263c1c367ddb335e9ae19c011b25cc"}, + {file = "mmh3-4.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:78c0ee0197cfc912f57172aa16e784ad55b533e2e2e91b3a65188cc66fbb1b6e"}, + {file = "mmh3-4.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d6acb15137467592691e41e6f897db1d2823ff3283111e316aa931ac0b5a5709"}, + {file = "mmh3-4.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f91b2598e1f25e013da070ff641a29ebda76292d3a7bdd20ef1736e9baf0de67"}, + {file = "mmh3-4.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a78f6f2592395321e2f0dc6b618773398b2c9b15becb419364e0960df53e9f04"}, + {file = "mmh3-4.0.1-cp38-cp38-win32.whl", hash = "sha256:d8650982d0b70af24700bd32b15fab33bb3ef9be4af411100f4960a938b0dd0f"}, + {file = "mmh3-4.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:2489949c7261870a02eeaa2ec7b966881c1775df847c8ce6ea4de3e9d96b5f4f"}, + {file = "mmh3-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:dcd03a4bb0fa3db03648d26fb221768862f089b6aec5272f0df782a8b4fe5b5b"}, + {file = "mmh3-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3775fb0cc675977e5b506b12b8f23cd220be3d4c2d4db7df81f03c9f61baa4cc"}, + {file = "mmh3-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f250f78328d41cdf73d3ad9809359636f4fb7a846d7a6586e1a0f0d2f5f2590"}, + {file = "mmh3-4.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4161009c9077d5ebf8b472dbf0f41b9139b3d380e0bbe71bf9b503efb2965584"}, + {file = "mmh3-4.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cf986ebf530717fefeee8d0decbf3f359812caebba985e2c8885c0ce7c2ee4e"}, + {file = "mmh3-4.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b55741ed51e928b1eec94a119e003fa3bc0139f4f9802e19bea3af03f7dd55a"}, + {file = "mmh3-4.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8250375641b8c5ce5d56a00c6bb29f583516389b8bde0023181d5eba8aa4119"}, + {file = "mmh3-4.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29373e802bc094ffd490e39047bac372ac893c0f411dac3223ef11775e34acd0"}, + {file = "mmh3-4.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:071ba41e56f5c385d13ee84b288ccaf46b70cd9e9a6d8cbcbe0964dee68c0019"}, + {file = "mmh3-4.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:909e0b88d2c6285481fa6895c2a0faf6384e1b0093f72791aa57d1e04f4adc65"}, + {file = "mmh3-4.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:51d356f4380f9d9c2a0612156c3d1e7359933991e84a19304440aa04fd723e68"}, + {file = "mmh3-4.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c4b2549949efa63d8decb6572f7e75fad4f2375d52fafced674323239dd9812d"}, + {file = "mmh3-4.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9bcc7b32a89c4e5c6fdef97d82e8087ba26a20c25b4aaf0723abd0b302525934"}, + {file = "mmh3-4.0.1-cp39-cp39-win32.whl", hash = "sha256:8edee21ae4f4337fb970810ef5a263e5d2212b85daca0d39daf995e13380e908"}, + {file = "mmh3-4.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8cbb6f90f08952fcc90dbf08f0310fdf4d61096c5cb7db8adf03e23f3b857ae5"}, + {file = "mmh3-4.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:ce71856cbca9d7c74d084eeee1bc5b126ed197c1c9530a4fdb994d099b9bc4db"}, + {file = "mmh3-4.0.1.tar.gz", hash = "sha256:ad8be695dc4e44a79631748ba5562d803f0ac42d36a6b97a53aca84a70809385"}, ] [package.extras] @@ -2226,13 +2282,13 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "platformdirs" -version = "3.8.0" +version = "3.9.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, - {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, + {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, + {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, ] [package.extras] @@ -2241,13 +2297,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest- [[package]] name = "prompt-toolkit" -version = "3.0.38" +version = "3.0.39" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"}, - {file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"}, + {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, + {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, ] [package.dependencies] @@ -2336,13 +2392,13 @@ files = [ [[package]] name = "pyaml" -version = "23.5.9" +version = "23.7.0" description = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data" optional = false python-versions = ">=3.8" files = [ - {file = "pyaml-23.5.9-py3-none-any.whl", hash = "sha256:b7fa20b43c5b6e5c8b7406a2408fe533efd65a6459feff828f918342f043ef4c"}, - {file = "pyaml-23.5.9.tar.gz", hash = "sha256:4c4b28b6fe89336000f08646f3cf1f6b68fb11e4c409626b77562e65a577273b"}, + {file = "pyaml-23.7.0-py3-none-any.whl", hash = "sha256:0a37018282545ccc31faecbe138fda4d89e236af04d691cfb5af00cd60089345"}, + {file = "pyaml-23.7.0.tar.gz", hash = "sha256:0c510bbb8938309400e0b1e47ac16fd90e56d652805a93417128786718f33546"}, ] [package.dependencies] @@ -2528,13 +2584,13 @@ six = ">=1.5" [[package]] name = "python-gnupg" -version = "0.5.0" +version = "0.5.1" description = "A wrapper for the Gnu Privacy Guard (GPG or GnuPG)" optional = false python-versions = "*" files = [ - {file = "python-gnupg-0.5.0.tar.gz", hash = "sha256:70758e387fc0e0c4badbcb394f61acbe68b34970a8fed7e0f7c89469fe17912a"}, - {file = "python_gnupg-0.5.0-py2.py3-none-any.whl", hash = "sha256:345723a03e67b82aba0ea8ae2328b2e4a3906fbe2c18c4082285c3b01068f270"}, + {file = "python-gnupg-0.5.1.tar.gz", hash = "sha256:5674bad4e93876c0b0d3197e314d7f942d39018bf31e2b833f6788a6813c3fb8"}, + {file = "python_gnupg-0.5.1-py2.py3-none-any.whl", hash = "sha256:bf9b2d9032ef38139b7d64184176cd0b293eaeae6e4f93f50e304c7051174482"}, ] [[package]] @@ -2579,51 +2635,51 @@ files = [ [[package]] name = "pyyaml" -version = "6.0" +version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] @@ -2683,20 +2739,20 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "responses" -version = "0.23.1" +version = "0.23.2" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.7" files = [ - {file = "responses-0.23.1-py3-none-any.whl", hash = "sha256:8a3a5915713483bf353b6f4079ba8b2a29029d1d1090a503c70b0dc5d9d0c7bd"}, - {file = "responses-0.23.1.tar.gz", hash = "sha256:c4d9aa9fc888188f0c673eff79a8dadbe2e75b7fe879dc80a221a06e0a68138f"}, + {file = "responses-0.23.2-py3-none-any.whl", hash = "sha256:9d49c218ba3079022bd63427f12b0a43b43d2f6aaf5ed859b9df9d733b4dd775"}, + {file = "responses-0.23.2.tar.gz", hash = "sha256:5d5a2ce3285f84e1f107d2e942476b6c7dff3747f289c0eae997cb77d2ab68e8"}, ] [package.dependencies] pyyaml = "*" -requests = ">=2.22.0,<3.0" +requests = ">=2.30.0,<3.0" types-PyYAML = "*" -urllib3 = ">=1.25.10" +urllib3 = ">=2.0.0,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] @@ -2899,13 +2955,13 @@ files = [ [[package]] name = "sphinx" -version = "7.0.1" +version = "7.1.0" description = "Python documentation generator" optional = false python-versions = ">=3.8" files = [ - {file = "Sphinx-7.0.1.tar.gz", hash = "sha256:61e025f788c5977d9412587e733733a289e2b9fdc2fef8868ddfbfc4ccfe881d"}, - {file = "sphinx-7.0.1-py3-none-any.whl", hash = "sha256:60c5e04756c1709a98845ed27a2eed7a556af3993afb66e77fec48189f742616"}, + {file = "sphinx-7.1.0-py3-none-any.whl", hash = "sha256:9bdfb5a2b28351d4fdf40a63cd006dbad727f793b243e669fc950d7116c634af"}, + {file = "sphinx-7.1.0.tar.gz", hash = "sha256:8f336d0221c3beb23006b3164ed1d46db9cebcce9cb41cdb9c5ecd4bcc509be0"}, ] [package.dependencies] @@ -3022,76 +3078,80 @@ test = ["pytest"] [[package]] name = "sqlalchemy" -version = "1.4.48" +version = "2.0.19" description = "Database Abstraction Library" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-1.4.48-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:4bac3aa3c3d8bc7408097e6fe8bf983caa6e9491c5d2e2488cfcfd8106f13b6a"}, - {file = "SQLAlchemy-1.4.48-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dbcae0e528d755f4522cad5842f0942e54b578d79f21a692c44d91352ea6d64e"}, - {file = "SQLAlchemy-1.4.48-cp27-cp27m-win32.whl", hash = "sha256:cbbe8b8bffb199b225d2fe3804421b7b43a0d49983f81dc654d0431d2f855543"}, - {file = "SQLAlchemy-1.4.48-cp27-cp27m-win_amd64.whl", hash = "sha256:627e04a5d54bd50628fc8734d5fc6df2a1aa5962f219c44aad50b00a6cdcf965"}, - {file = "SQLAlchemy-1.4.48-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9af1db7a287ef86e0f5cd990b38da6bd9328de739d17e8864f1817710da2d217"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ce7915eecc9c14a93b73f4e1c9d779ca43e955b43ddf1e21df154184f39748e5"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5381ddd09a99638f429f4cbe1b71b025bed318f6a7b23e11d65f3eed5e181c33"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:87609f6d4e81a941a17e61a4c19fee57f795e96f834c4f0a30cee725fc3f81d9"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb0808ad34167f394fea21bd4587fc62f3bd81bba232a1e7fbdfa17e6cfa7cd7"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-win32.whl", hash = "sha256:d53cd8bc582da5c1c8c86b6acc4ef42e20985c57d0ebc906445989df566c5603"}, - {file = "SQLAlchemy-1.4.48-cp310-cp310-win_amd64.whl", hash = "sha256:4355e5915844afdc5cf22ec29fba1010166e35dd94a21305f49020022167556b"}, - {file = "SQLAlchemy-1.4.48-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:066c2b0413e8cb980e6d46bf9d35ca83be81c20af688fedaef01450b06e4aa5e"}, - {file = "SQLAlchemy-1.4.48-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c99bf13e07140601d111a7c6f1fc1519914dd4e5228315bbda255e08412f61a4"}, - {file = "SQLAlchemy-1.4.48-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee26276f12614d47cc07bc85490a70f559cba965fb178b1c45d46ffa8d73fda"}, - {file = "SQLAlchemy-1.4.48-cp311-cp311-win32.whl", hash = "sha256:49c312bcff4728bffc6fb5e5318b8020ed5c8b958a06800f91859fe9633ca20e"}, - {file = "SQLAlchemy-1.4.48-cp311-cp311-win_amd64.whl", hash = "sha256:cef2e2abc06eab187a533ec3e1067a71d7bbec69e582401afdf6d8cad4ba3515"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3509159e050bd6d24189ec7af373359f07aed690db91909c131e5068176c5a5d"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc2ab4d9f6d9218a5caa4121bdcf1125303482a1cdcfcdbd8567be8518969c0"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1ddbbcef9bcedaa370c03771ebec7e39e3944782bef49e69430383c376a250b"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f82d8efea1ca92b24f51d3aea1a82897ed2409868a0af04247c8c1e4fef5890"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-win32.whl", hash = "sha256:e3e98d4907805b07743b583a99ecc58bf8807ecb6985576d82d5e8ae103b5272"}, - {file = "SQLAlchemy-1.4.48-cp36-cp36m-win_amd64.whl", hash = "sha256:25887b4f716e085a1c5162f130b852f84e18d2633942c8ca40dfb8519367c14f"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0817c181271b0ce5df1aa20949f0a9e2426830fed5ecdcc8db449618f12c2730"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe1dd2562313dd9fe1778ed56739ad5d9aae10f9f43d9f4cf81d65b0c85168bb"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:68413aead943883b341b2b77acd7a7fe2377c34d82e64d1840860247cec7ff7c"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbde5642104ac6e95f96e8ad6d18d9382aa20672008cf26068fe36f3004491df"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-win32.whl", hash = "sha256:11c6b1de720f816c22d6ad3bbfa2f026f89c7b78a5c4ffafb220e0183956a92a"}, - {file = "SQLAlchemy-1.4.48-cp37-cp37m-win_amd64.whl", hash = "sha256:eb5464ee8d4bb6549d368b578e9529d3c43265007193597ddca71c1bae6174e6"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:92e6133cf337c42bfee03ca08c62ba0f2d9695618c8abc14a564f47503157be9"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d29a3fc6d9c45962476b470a81983dd8add6ad26fdbfae6d463b509d5adcda"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:005e942b451cad5285015481ae4e557ff4154dde327840ba91b9ac379be3b6ce"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8cfe951ed074ba5e708ed29c45397a95c4143255b0d022c7c8331a75ae61f3"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-win32.whl", hash = "sha256:2b9af65cc58726129d8414fc1a1a650dcdd594ba12e9c97909f1f57d48e393d3"}, - {file = "SQLAlchemy-1.4.48-cp38-cp38-win_amd64.whl", hash = "sha256:2b562e9d1e59be7833edf28b0968f156683d57cabd2137d8121806f38a9d58f4"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:a1fc046756cf2a37d7277c93278566ddf8be135c6a58397b4c940abf837011f4"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d9b55252d2ca42a09bcd10a697fa041e696def9dfab0b78c0aaea1485551a08"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6dab89874e72a9ab5462997846d4c760cdb957958be27b03b49cf0de5e5c327c"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd8b5ee5a3acc4371f820934b36f8109ce604ee73cc668c724abb054cebcb6e"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-win32.whl", hash = "sha256:eee09350fd538e29cfe3a496ec6f148504d2da40dbf52adefb0d2f8e4d38ccc4"}, - {file = "SQLAlchemy-1.4.48-cp39-cp39-win_amd64.whl", hash = "sha256:7ad2b0f6520ed5038e795cc2852eb5c1f20fa6831d73301ced4aafbe3a10e1f6"}, - {file = "SQLAlchemy-1.4.48.tar.gz", hash = "sha256:b47bc287096d989a0838ce96f7d8e966914a24da877ed41a7531d44b55cdb8df"}, -] - -[package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"} + {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9deaae357edc2091a9ed5d25e9ee8bba98bcfae454b3911adeaf159c2e9ca9e3"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bf0fd65b50a330261ec7fe3d091dfc1c577483c96a9fa1e4323e932961aa1b5"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d90ccc15ba1baa345796a8fb1965223ca7ded2d235ccbef80a47b85cea2d71a"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4e688f6784427e5f9479d1a13617f573de8f7d4aa713ba82813bcd16e259d1"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:584f66e5e1979a7a00f4935015840be627e31ca29ad13f49a6e51e97a3fb8cae"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c69ce70047b801d2aba3e5ff3cba32014558966109fecab0c39d16c18510f15"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-win32.whl", hash = "sha256:96f0463573469579d32ad0c91929548d78314ef95c210a8115346271beeeaaa2"}, + {file = "SQLAlchemy-2.0.19-cp310-cp310-win_amd64.whl", hash = "sha256:22bafb1da60c24514c141a7ff852b52f9f573fb933b1e6b5263f0daa28ce6db9"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6894708eeb81f6d8193e996257223b6bb4041cb05a17cd5cf373ed836ef87a2"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f2afd1aafded7362b397581772c670f20ea84d0a780b93a1a1529da7c3d369"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15afbf5aa76f2241184c1d3b61af1a72ba31ce4161013d7cb5c4c2fca04fd6e"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc05b59142445a4efb9c1fd75c334b431d35c304b0e33f4fa0ff1ea4890f92e"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5831138f0cc06b43edf5f99541c64adf0ab0d41f9a4471fd63b54ae18399e4de"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3afa8a21a9046917b3a12ffe016ba7ebe7a55a6fc0c7d950beb303c735c3c3ad"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-win32.whl", hash = "sha256:c896d4e6ab2eba2afa1d56be3d0b936c56d4666e789bfc59d6ae76e9fcf46145"}, + {file = "SQLAlchemy-2.0.19-cp311-cp311-win_amd64.whl", hash = "sha256:024d2f67fb3ec697555e48caeb7147cfe2c08065a4f1a52d93c3d44fc8e6ad1c"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:89bc2b374ebee1a02fd2eae6fd0570b5ad897ee514e0f84c5c137c942772aa0c"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4d410a76c3762511ae075d50f379ae09551d92525aa5bb307f8343bf7c2c12"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f469f15068cd8351826df4080ffe4cc6377c5bf7d29b5a07b0e717dddb4c7ea2"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cda283700c984e699e8ef0fcc5c61f00c9d14b6f65a4f2767c97242513fcdd84"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:43699eb3f80920cc39a380c159ae21c8a8924fe071bccb68fc509e099420b148"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-win32.whl", hash = "sha256:61ada5831db36d897e28eb95f0f81814525e0d7927fb51145526c4e63174920b"}, + {file = "SQLAlchemy-2.0.19-cp37-cp37m-win_amd64.whl", hash = "sha256:57d100a421d9ab4874f51285c059003292433c648df6abe6c9c904e5bd5b0828"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16a310f5bc75a5b2ce7cb656d0e76eb13440b8354f927ff15cbaddd2523ee2d1"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf7b5e3856cbf1876da4e9d9715546fa26b6e0ba1a682d5ed2fc3ca4c7c3ec5b"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e7b69d9ced4b53310a87117824b23c509c6fc1f692aa7272d47561347e133b6"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9eb4575bfa5afc4b066528302bf12083da3175f71b64a43a7c0badda2be365"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6b54d1ad7a162857bb7c8ef689049c7cd9eae2f38864fc096d62ae10bc100c7d"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5d6afc41ca0ecf373366fd8e10aee2797128d3ae45eb8467b19da4899bcd1ee0"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-win32.whl", hash = "sha256:430614f18443b58ceb9dedec323ecddc0abb2b34e79d03503b5a7579cd73a531"}, + {file = "SQLAlchemy-2.0.19-cp38-cp38-win_amd64.whl", hash = "sha256:eb60699de43ba1a1f77363f563bb2c652f7748127ba3a774f7cf2c7804aa0d3d"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a752b7a9aceb0ba173955d4f780c64ee15a1a991f1c52d307d6215c6c73b3a4c"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7351c05db355da112e056a7b731253cbeffab9dfdb3be1e895368513c7d70106"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa51ce4aea583b0c6b426f4b0563d3535c1c75986c4373a0987d84d22376585b"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae7473a67cd82a41decfea58c0eac581209a0aa30f8bc9190926fbf628bb17f7"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:851a37898a8a39783aab603c7348eb5b20d83c76a14766a43f56e6ad422d1ec8"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539010665c90e60c4a1650afe4ab49ca100c74e6aef882466f1de6471d414be7"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-win32.whl", hash = "sha256:f82c310ddf97b04e1392c33cf9a70909e0ae10a7e2ddc1d64495e3abdc5d19fb"}, + {file = "SQLAlchemy-2.0.19-cp39-cp39-win_amd64.whl", hash = "sha256:8e712cfd2e07b801bc6b60fdf64853bc2bd0af33ca8fa46166a23fe11ce0dbb0"}, + {file = "SQLAlchemy-2.0.19-py3-none-any.whl", hash = "sha256:314145c1389b021a9ad5aa3a18bac6f5d939f9087d7fc5443be28cba19d2c972"}, + {file = "SQLAlchemy-2.0.19.tar.gz", hash = "sha256:77a14fa20264af73ddcdb1e2b9c5a829b8cc6b8304d0f093271980e36c200a3f"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} +typing-extensions = ">=4.2.0" [package.extras] aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] -mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"] +oracle = ["cx-oracle (>=7)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] -postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql", "pymysql (<1)"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] sqlcipher = ["sqlcipher3-binary"] [[package]] @@ -3124,13 +3184,13 @@ url = ["furl (>=0.4.1)"] [[package]] name = "sqlalchemy2-stubs" -version = "0.0.2a34" +version = "0.0.2a35" description = "Typing Stubs for SQLAlchemy 1.4" optional = false python-versions = ">=3.6" files = [ - {file = "sqlalchemy2-stubs-0.0.2a34.tar.gz", hash = "sha256:2432137ab2fde1a608df4544f6712427b0b7ff25990cfbbc5a9d1db6c8c6f489"}, - {file = "sqlalchemy2_stubs-0.0.2a34-py3-none-any.whl", hash = "sha256:a313220ac793404349899faf1272e821a62dbe1d3a029bd444faa8d3e966cd07"}, + {file = "sqlalchemy2-stubs-0.0.2a35.tar.gz", hash = "sha256:bd5d530697d7e8c8504c7fe792ef334538392a5fb7aa7e4f670bfacdd668a19d"}, + {file = "sqlalchemy2_stubs-0.0.2a35-py3-none-any.whl", hash = "sha256:593784ff9fc0dc2ded1895e3322591689db3be06f3ca006e3ef47640baf2d38a"}, ] [package.dependencies] @@ -3138,13 +3198,13 @@ typing-extensions = ">=3.7.4" [[package]] name = "trio" -version = "0.22.1" +version = "0.22.2" description = "A friendly Python library for async concurrency and I/O" optional = false python-versions = ">=3.7" files = [ - {file = "trio-0.22.1-py3-none-any.whl", hash = "sha256:1270da4a4a33caf33f85c6a255f2ef5f71629a3ec9aea31a052062b673ae58d3"}, - {file = "trio-0.22.1.tar.gz", hash = "sha256:eb5f641b313eb502a16de176d84cecd9ccf2394a7c8655d2297398376bb15eca"}, + {file = "trio-0.22.2-py3-none-any.whl", hash = "sha256:f43da357620e5872b3d940a2e3589aa251fd3f881b65a608d742e00809b1ec38"}, + {file = "trio-0.22.2.tar.gz", hash = "sha256:3887cf18c8bcc894433420305468388dac76932e9668afa1c49aa3806b6accb3"}, ] [package.dependencies] @@ -3183,13 +3243,13 @@ files = [ [[package]] name = "types-bleach" -version = "6.0.0.3" +version = "6.0.0.4" description = "Typing stubs for bleach" optional = false python-versions = "*" files = [ - {file = "types-bleach-6.0.0.3.tar.gz", hash = "sha256:8ce7896d4f658c562768674ffcf07492c7730e128018f03edd163ff912bfadee"}, - {file = "types_bleach-6.0.0.3-py3-none-any.whl", hash = "sha256:d43eaf30a643ca824e16e2dcdb0c87ef9226237e2fa3ac4732a50cb3f32e145f"}, + {file = "types-bleach-6.0.0.4.tar.gz", hash = "sha256:357b0226f65c4f20ab3b13ca8d78a6b91c78aad256d8ec168d4e90fc3303ebd4"}, + {file = "types_bleach-6.0.0.4-py3-none-any.whl", hash = "sha256:2b8767eb407c286b7f02803678732e522e04db8d56cbc9f1270bee49627eae92"}, ] [[package]] @@ -3205,13 +3265,13 @@ files = [ [[package]] name = "types-pyopenssl" -version = "23.2.0.1" +version = "23.2.0.2" description = "Typing stubs for pyOpenSSL" optional = false python-versions = "*" files = [ - {file = "types-pyOpenSSL-23.2.0.1.tar.gz", hash = "sha256:beeb5d22704c625a1e4b6dc756355c5b4af0b980138b702a9d9f932acf020903"}, - {file = "types_pyOpenSSL-23.2.0.1-py3-none-any.whl", hash = "sha256:0568553f104466f1b8e0db3360fbe6770137d02e21a1a45c209bf2b1b03d90d4"}, + {file = "types-pyOpenSSL-23.2.0.2.tar.gz", hash = "sha256:6a010dac9ecd42b582d7dd2cc3e9e40486b79b3b64bb2fffba1474ff96af906d"}, + {file = "types_pyOpenSSL-23.2.0.2-py3-none-any.whl", hash = "sha256:19536aa3debfbe25a918cf0d898e9f5fbbe6f3594a429da7914bf331deb1b342"}, ] [package.dependencies] @@ -3233,13 +3293,13 @@ dev = ["mypy (==0.991)", "pipenv-setup (==3.2.0)", "pysaml2 (==7.2.1)", "twine ( [[package]] name = "types-python-dateutil" -version = "2.8.19.13" +version = "2.8.19.14" description = "Typing stubs for python-dateutil" optional = false python-versions = "*" files = [ - {file = "types-python-dateutil-2.8.19.13.tar.gz", hash = "sha256:09a0275f95ee31ce68196710ed2c3d1b9dc42e0b61cc43acc369a42cb939134f"}, - {file = "types_python_dateutil-2.8.19.13-py3-none-any.whl", hash = "sha256:0b0e7c68e7043b0354b26a1e0225cb1baea7abb1b324d02b50e2d08f1221043f"}, + {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, + {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, ] [[package]] @@ -3255,24 +3315,24 @@ files = [ [[package]] name = "types-pyyaml" -version = "6.0.12.10" +version = "6.0.12.11" description = "Typing stubs for PyYAML" optional = false python-versions = "*" files = [ - {file = "types-PyYAML-6.0.12.10.tar.gz", hash = "sha256:ebab3d0700b946553724ae6ca636ea932c1b0868701d4af121630e78d695fc97"}, - {file = "types_PyYAML-6.0.12.10-py3-none-any.whl", hash = "sha256:662fa444963eff9b68120d70cda1af5a5f2aa57900003c2006d7626450eaae5f"}, + {file = "types-PyYAML-6.0.12.11.tar.gz", hash = "sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b"}, + {file = "types_PyYAML-6.0.12.11-py3-none-any.whl", hash = "sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d"}, ] [[package]] name = "types-redis" -version = "4.6.0.1" +version = "4.6.0.3" description = "Typing stubs for redis" optional = false python-versions = "*" files = [ - {file = "types-redis-4.6.0.1.tar.gz", hash = "sha256:1254d525de7a45e2efaacb6969e67ad1dd5cc359a092022200583a3f04868669"}, - {file = "types_redis-4.6.0.1-py3-none-any.whl", hash = "sha256:88ceb79c27f2084ad6f0b8514f8fcd8a740811f07c25f3fef5c9e843fc6c60a2"}, + {file = "types-redis-4.6.0.3.tar.gz", hash = "sha256:efdef37dc0c04bf5786195651fd694f8bfdd693eac09ec4af46d90f72652558f"}, + {file = "types_redis-4.6.0.3-py3-none-any.whl", hash = "sha256:67c44c14369c33c2a300da2a50b5607c0fc888f7b85eeb7c73e15c78a0f05edd"}, ] [package.dependencies] @@ -3281,13 +3341,13 @@ types-pyOpenSSL = "*" [[package]] name = "types-requests" -version = "2.31.0.1" +version = "2.31.0.2" description = "Typing stubs for requests" optional = false python-versions = "*" files = [ - {file = "types-requests-2.31.0.1.tar.gz", hash = "sha256:3de667cffa123ce698591de0ad7db034a5317457a596eb0b4944e5a9d9e8d1ac"}, - {file = "types_requests-2.31.0.1-py3-none-any.whl", hash = "sha256:afb06ef8f25ba83d59a1d424bd7a5a939082f94b94e90ab5e6116bd2559deaa3"}, + {file = "types-requests-2.31.0.2.tar.gz", hash = "sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40"}, + {file = "types_requests-2.31.0.2-py3-none-any.whl", hash = "sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a"}, ] [package.dependencies] @@ -3295,13 +3355,13 @@ types-urllib3 = "*" [[package]] name = "types-urllib3" -version = "1.26.25.13" +version = "1.26.25.14" description = "Typing stubs for urllib3" optional = false python-versions = "*" files = [ - {file = "types-urllib3-1.26.25.13.tar.gz", hash = "sha256:3300538c9dc11dad32eae4827ac313f5d986b8b21494801f1bf97a1ac6c03ae5"}, - {file = "types_urllib3-1.26.25.13-py3-none-any.whl", hash = "sha256:5dbd1d2bef14efee43f5318b5d36d805a489f6600252bb53626d4bfafd95e27c"}, + {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, + {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, ] [[package]] @@ -3353,13 +3413,13 @@ files = [ [[package]] name = "urllib3" -version = "2.0.3" +version = "2.0.4" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ - {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"}, - {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"}, + {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, + {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, ] [package.dependencies] @@ -3487,17 +3547,17 @@ watchdog = ["watchdog (>=2.3)"] [[package]] name = "wheel" -version = "0.40.0" +version = "0.41.0" description = "A built-package format for Python" optional = false python-versions = ">=3.7" files = [ - {file = "wheel-0.40.0-py3-none-any.whl", hash = "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247"}, - {file = "wheel-0.40.0.tar.gz", hash = "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873"}, + {file = "wheel-0.41.0-py3-none-any.whl", hash = "sha256:7e9be3bbd0078f6147d82ed9ed957e323e7708f57e134743d2edef3a7b7972a9"}, + {file = "wheel-0.41.0.tar.gz", hash = "sha256:55a0f0a5a84869bce5ba775abfd9c462e3a6b1b7b7ec69d72c0b83d673a5114d"}, ] [package.extras] -test = ["pytest (>=6.0.0)"] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [[package]] name = "wsproto" @@ -3704,4 +3764,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "48dc1f000dc504a52e0c0f5a2e26a372ed0ab67b4192f78c035f3f8c96d0a623" +content-hash = "d63b45f79f6a7f489f0a9e607725117856d97434a9f8fc22f53c764297da00db" diff --git a/pyproject.toml b/pyproject.toml index c2b5ad8f6f..9a5182dd32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ python = "^3.11" flask = "^2.3.2" lxml = "^4.9.2" webargs = "5.5" -wheel = "^0.40.0" +wheel = "^0.41.0" setuptools = "^68.0.0" attrs = "^23.1.0" authlib = "^1.2.1" @@ -34,9 +34,9 @@ Flask-OpenID = "^1.3.0" Flask-SQLAlchemy = "^3.0.5" Flask-Testing = "^0.8.1" Flask-WTF = "^1.0.1" -gevent = "^22.10.2" +gevent = "^23.7.0" webassets = { git = "https://github.com/miracle2k/webassets.git" } -gunicorn = "^20.1.0" +gunicorn = "^21.2.0" html5lib = "^1.1" httpagentparser = "^1.9.2" humanize = "^4.1.0" @@ -62,7 +62,7 @@ recommonmark = "^0.7.1" responses = "^0.23.1" selenium = "^4.2.0" Sphinx = "^7.0.1" -SQLAlchemy = "<2.0.0" +SQLAlchemy = "^2.0.19" SQLAlchemy-Utils = "^0.41.1" typing-inspect = "^0.9.0" voikko = "^0.5" From 9fdb3c7f18a8fe63b57539866a642e22f128412d Mon Sep 17 00:00:00 2001 From: dezhidki Date: Wed, 26 Jul 2023 22:29:05 +0300 Subject: [PATCH 07/34] Convert Column to mapped_column columns in DB models --- timApp/answer/answer.py | 32 ++-- timApp/answer/answer_models.py | 26 +-- timApp/auth/auth_models.py | 28 ++-- timApp/auth/oauth2/models.py | 13 +- timApp/auth/session/model.py | 15 +- timApp/celery_sqlalchemy_scheduler/models.py | 86 +++++----- timApp/document/docentry.py | 15 +- timApp/document/translation/deepl.py | 19 ++- timApp/document/translation/language.py | 12 +- timApp/document/translation/translation.py | 9 +- timApp/document/translation/translator.py | 22 +-- timApp/folder/folder.py | 9 +- timApp/item/block.py | 43 +++-- timApp/item/blockassociation.py | 8 +- timApp/item/blockrelevance.py | 8 +- timApp/item/routes.py | 10 +- timApp/item/tag.py | 12 +- timApp/item/taskblock.py | 7 +- timApp/lecture/askedjson.py | 9 +- timApp/lecture/askedquestion.py | 29 ++-- timApp/lecture/lecture.py | 31 ++-- timApp/lecture/lectureanswer.py | 18 +- timApp/lecture/lectureusers.py | 8 +- timApp/lecture/message.py | 14 +- timApp/lecture/question.py | 20 ++- timApp/lecture/questionactivity.py | 12 +- timApp/lecture/runningquestion.py | 17 +- timApp/lecture/showpoints.py | 6 +- timApp/lecture/useractivity.py | 13 +- .../messagelist/messagelist_models.py | 83 +++++----- .../timMessage/internalmessage_models.py | 45 ++--- timApp/note/usernote.py | 29 ++-- timApp/notification/notification.py | 16 +- timApp/notification/pending_notification.py | 34 ++-- timApp/peerreview/peerreview.py | 26 +-- timApp/plugin/calendar/models.py | 95 +++++------ timApp/plugin/plugintype.py | 7 +- timApp/plugin/timtable/row_owner_info.py | 12 +- timApp/printing/printeddoc.py | 23 ++- timApp/readmark/readparagraph.py | 21 +-- timApp/sisu/scimusergroup.py | 8 +- timApp/slide/slidestatus.py | 8 +- timApp/tim.py | 37 +++-- timApp/timdb/sqa.py | 5 +- timApp/user/consentchange.py | 11 +- timApp/user/hakaorganization.py | 7 +- timApp/user/newuser.py | 11 +- timApp/user/personaluniquecode.py | 11 +- timApp/user/user.py | 71 ++++---- timApp/user/usercontact.py | 20 ++- timApp/user/usergroup.py | 44 +++-- timApp/user/usergroupdoc.py | 8 +- timApp/user/usergroupmember.py | 17 +- timApp/user/verification/verification.py | 22 +-- timApp/velp/annotation_model.py | 54 +++--- timApp/velp/annotations.py | 63 ++++--- timApp/velp/velp_models.py | 154 ++++++++++-------- 57 files changed, 794 insertions(+), 699 deletions(-) diff --git a/timApp/answer/answer.py b/timApp/answer/answer.py index e72423deb0..943d72d89f 100644 --- a/timApp/answer/answer.py +++ b/timApp/answer/answer.py @@ -2,9 +2,9 @@ from typing import Any from sqlalchemy import func +from sqlalchemy.orm import mapped_column from timApp.answer.answer_models import UserAnswer -from timApp.plugin.plugintype import PluginType from timApp.plugin.taskid import TaskId from timApp.timdb.sqa import db, include_if_loaded @@ -15,50 +15,52 @@ class AnswerSaver(db.Model): """ __tablename__ = "answersaver" - __allow_unmapped__ = True + - answer_id = db.Column(db.Integer, db.ForeignKey("answer.id"), primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) + answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id"), primary_key=True) + user_id = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + ) class Answer(db.Model): """An answer to a task.""" __tablename__ = "answer" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) """Answer identifier.""" - task_id = db.Column(db.Text, nullable=False, index=True) + task_id = mapped_column(db.Text, nullable=False, index=True) """Task id to which this answer was posted. In the form "doc_id.name", for example "2.task1".""" - origin_doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=True) + origin_doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=True) """The document in which the answer was saved""" - plugin_type_id = db.Column( + plugin_type_id = mapped_column( db.Integer, db.ForeignKey("plugintype.id"), nullable=True ) """Plugin type the answer was saved on""" - content = db.Column(db.Text, nullable=False) + content = mapped_column(db.Text, nullable=False) """Answer content.""" - points = db.Column(db.Float) + points = mapped_column(db.Float) """Points.""" - answered_on = db.Column( + answered_on = mapped_column( db.DateTime(timezone=True), nullable=False, default=func.now() ) """Answer timestamp.""" - valid = db.Column(db.Boolean, nullable=False) + valid = mapped_column(db.Boolean, nullable=False) """Whether this answer is valid.""" - last_points_modifier = db.Column(db.Integer, db.ForeignKey("usergroup.id")) + last_points_modifier = mapped_column(db.Integer, db.ForeignKey("usergroup.id")) """The UserGroup who modified the points last. Null if the points have been given by the task automatically.""" - plugin_type: PluginType | None = db.relationship("PluginType", lazy="select") + plugin_type = db.relationship("PluginType", lazy="select") # : PluginType | None uploads = db.relationship("AnswerUpload", back_populates="answer", lazy="dynamic") users = db.relationship( "User", secondary=UserAnswer.__table__, back_populates="answers", lazy="dynamic" diff --git a/timApp/answer/answer_models.py b/timApp/answer/answer_models.py index 819212b8cb..eef194fcc6 100644 --- a/timApp/answer/answer_models.py +++ b/timApp/answer/answer_models.py @@ -1,3 +1,5 @@ +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db @@ -8,21 +10,23 @@ class AnswerTag(db.Model): """ __tablename__ = "answertag" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) - answer_id = db.Column(db.Integer, db.ForeignKey("answer.id"), nullable=False) - tag = db.Column(db.Text, nullable=False) + id = mapped_column(db.Integer, primary_key=True) + answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id"), nullable=False) + tag = mapped_column(db.Text, nullable=False) class AnswerUpload(db.Model): """Associates uploaded files (Block with type BlockType.AnswerUpload) with Answers.""" __tablename__ = "answerupload" - __allow_unmapped__ = True + - upload_block_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - answer_id = db.Column(db.Integer, db.ForeignKey("answer.id")) + upload_block_id = mapped_column( + db.Integer, db.ForeignKey("block.id"), primary_key=True + ) + answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id")) block = db.relationship("Block", back_populates="answerupload") answer = db.relationship("Answer", back_populates="uploads") @@ -36,9 +40,9 @@ class UserAnswer(db.Model): """Associates Users with Answers.""" __tablename__ = "useranswer" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) - answer_id = db.Column(db.Integer, db.ForeignKey("answer.id"), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + + id = mapped_column(db.Integer, primary_key=True) + answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id"), nullable=False) + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) __table_args__ = (db.UniqueConstraint("answer_id", "user_id"),) diff --git a/timApp/auth/auth_models.py b/timApp/auth/auth_models.py index 5ae7358b13..fdbad59d02 100644 --- a/timApp/auth/auth_models.py +++ b/timApp/auth/auth_models.py @@ -3,6 +3,8 @@ from datetime import datetime from typing import TYPE_CHECKING +from sqlalchemy.orm import mapped_column + from timApp.auth.accesstype import AccessType if TYPE_CHECKING: @@ -16,12 +18,12 @@ class AccessTypeModel(db.Model): """A kind of access that a UserGroup may have to a Block.""" __tablename__ = "accesstype" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) """Access type identifier.""" - name = db.Column(db.Text, nullable=False) + name = mapped_column(db.Text, nullable=False) """Access type name, such as 'view', 'edit', 'manage', etc.""" accesses = db.relationship("BlockAccess", back_populates="atype") @@ -37,19 +39,19 @@ class BlockAccess(db.Model): """A single permission. Relates a UserGroup with a Block along with an AccessType.""" __tablename__ = "blockaccess" - __allow_unmapped__ = True - block_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - usergroup_id = db.Column( + + block_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + usergroup_id = mapped_column( db.Integer, db.ForeignKey("usergroup.id"), primary_key=True ) - type = db.Column(db.Integer, db.ForeignKey("accesstype.id"), primary_key=True) - accessible_from = db.Column(db.DateTime(timezone=True)) - accessible_to = db.Column(db.DateTime(timezone=True)) - duration = db.Column(db.Interval) - duration_from = db.Column(db.DateTime(timezone=True)) - duration_to = db.Column(db.DateTime(timezone=True)) - require_confirm = db.Column(db.Boolean) + type = mapped_column(db.Integer, db.ForeignKey("accesstype.id"), primary_key=True) + accessible_from = mapped_column(db.DateTime(timezone=True)) + accessible_to = mapped_column(db.DateTime(timezone=True)) + duration = mapped_column(db.Interval) + duration_from = mapped_column(db.DateTime(timezone=True)) + duration_to = mapped_column(db.DateTime(timezone=True)) + require_confirm = mapped_column(db.Boolean) block = db.relationship("Block", back_populates="accesses") usergroup = db.relationship("UserGroup", back_populates="accesses") diff --git a/timApp/auth/oauth2/models.py b/timApp/auth/oauth2/models.py index 3adf413874..1c7096a780 100644 --- a/timApp/auth/oauth2/models.py +++ b/timApp/auth/oauth2/models.py @@ -6,6 +6,7 @@ OAuth2AuthorizationCodeMixin, ) from authlib.oauth2.rfc6749 import ClientMixin, scope_to_list, list_to_scope +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db @@ -102,17 +103,17 @@ def check_grant_type(self, grant_type: str) -> bool: class OAuth2Token(db.Model, OAuth2TokenMixin): __tablename__ = "oauth2_token" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id")) + id = mapped_column(db.Integer, primary_key=True) + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id")) user = db.relationship("User") class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): __tablename__ = "oauth2_auth_code" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id")) + + id = mapped_column(db.Integer, primary_key=True) + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id")) user = db.relationship("User") diff --git a/timApp/auth/session/model.py b/timApp/auth/session/model.py index 7c92e56a27..e1b198b6ed 100755 --- a/timApp/auth/session/model.py +++ b/timApp/auth/session/model.py @@ -1,9 +1,9 @@ """ Database models for session management. """ -from datetime import datetime from sqlalchemy.ext.hybrid import hybrid_property # type: ignore +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db from timApp.util.utils import get_current_time @@ -20,29 +20,30 @@ class UserSession(db.Model): """ __tablename__ = "usersession" - __allow_unmapped__ = True - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) + user_id = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + ) """ User ID of the user who owns the session. """ - session_id = db.Column(db.Text, primary_key=True) + session_id = mapped_column(db.Text, primary_key=True) """ Unique session ID. """ - logged_in_at = db.Column(db.DateTime, nullable=False, default=get_current_time) + logged_in_at = mapped_column(db.DateTime, nullable=False, default=get_current_time) """ The time when the user logged in and the session was created. """ - expired_at: datetime | None = db.Column(db.DateTime, nullable=True) + expired_at = mapped_column(db.DateTime, nullable=True) """ The time when the session was expired. """ - origin = db.Column(db.Text, nullable=False) + origin = mapped_column(db.Text, nullable=False) """ Information about the origin of the session. May include user agent and any other information about login state. diff --git a/timApp/celery_sqlalchemy_scheduler/models.py b/timApp/celery_sqlalchemy_scheduler/models.py index eda290dbfb..d4f6275b03 100644 --- a/timApp/celery_sqlalchemy_scheduler/models.py +++ b/timApp/celery_sqlalchemy_scheduler/models.py @@ -6,7 +6,7 @@ from celery.utils.log import get_logger from sqlalchemy import func from sqlalchemy.event import listen -from sqlalchemy.orm import relationship, foreign, remote +from sqlalchemy.orm import relationship, foreign, remote, mapped_column from sqlalchemy.sql import select, insert, update from .session import ModelBase @@ -37,7 +37,7 @@ def update(self, **kw): class IntervalSchedule(ModelBase, ModelMixin): __tablename__ = "celery_interval_schedule" __table_args__ = {"sqlite_autoincrement": True} - __allow_unmapped__ = True + DAYS = "days" HOURS = "hours" @@ -45,10 +45,10 @@ class IntervalSchedule(ModelBase, ModelMixin): SECONDS = "seconds" MICROSECONDS = "microseconds" - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) + id = mapped_column(sa.Integer, primary_key=True, autoincrement=True) - every = sa.Column(sa.Integer, nullable=False) - period = sa.Column(sa.String(24)) + every = mapped_column(sa.Integer, nullable=False) + period = mapped_column(sa.String(24)) def __repr__(self): if self.every == 1: @@ -87,15 +87,15 @@ def period_singular(self): class CrontabSchedule(ModelBase, ModelMixin): __tablename__ = "celery_crontab_schedule" __table_args__ = {"sqlite_autoincrement": True} - __allow_unmapped__ = True + - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) - minute = sa.Column(sa.String(60 * 4), default="*") - hour = sa.Column(sa.String(24 * 4), default="*") - day_of_week = sa.Column(sa.String(64), default="*") - day_of_month = sa.Column(sa.String(31 * 4), default="*") - month_of_year = sa.Column(sa.String(64), default="*") - timezone = sa.Column(sa.String(64), default="UTC") + id = mapped_column(sa.Integer, primary_key=True, autoincrement=True) + minute = mapped_column(sa.String(60 * 4), default="*") + hour = mapped_column(sa.String(24 * 4), default="*") + day_of_week = mapped_column(sa.String(64), default="*") + day_of_month = mapped_column(sa.String(31 * 4), default="*") + month_of_year = mapped_column(sa.String(64), default="*") + timezone = mapped_column(sa.String(64), default="UTC") def __repr__(self): return "{} {} {} {} {} (m/h/d/dM/MY) {}".format( @@ -144,13 +144,13 @@ def from_schedule(cls, session, schedule): class SolarSchedule(ModelBase, ModelMixin): __tablename__ = "celery_solar_schedule" __table_args__ = {"sqlite_autoincrement": True} - __allow_unmapped__ = True + - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) + id = mapped_column(sa.Integer, primary_key=True, autoincrement=True) - event = sa.Column(sa.String(24)) - latitude = sa.Column(sa.Float()) - longitude = sa.Column(sa.Float()) + event = mapped_column(sa.String(24)) + latitude = mapped_column(sa.Float()) + longitude = mapped_column(sa.Float()) @property def schedule(self): @@ -182,10 +182,10 @@ class PeriodicTaskChanged(ModelBase, ModelMixin): """Helper table for tracking updates to periodic tasks.""" __tablename__ = "celery_periodic_task_changed" - __allow_unmapped__ = True + - id = sa.Column(sa.Integer, primary_key=True) - last_update = sa.Column( + id = mapped_column(sa.Integer, primary_key=True) + last_update = mapped_column( sa.DateTime(timezone=True), nullable=False, default=dt.datetime.now ) @@ -234,60 +234,60 @@ def last_change(cls, session): class PeriodicTask(ModelBase, ModelMixin): __tablename__ = "celery_periodic_task" __table_args__ = {"sqlite_autoincrement": True} - __allow_unmapped__ = True + - id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) - block_id = sa.Column(sa.Integer, sa.ForeignKey("block.id"), nullable=True) + id = mapped_column(sa.Integer, primary_key=True, autoincrement=True) + block_id = mapped_column(sa.Integer, sa.ForeignKey("block.id"), nullable=True) block = relationship(Block) # name - name = sa.Column(sa.String(255), unique=True) + name = mapped_column(sa.String(255), unique=True) # task name - task = sa.Column(sa.String(255)) + task = mapped_column(sa.String(255)) # not use ForeignKey - interval_id = sa.Column(sa.Integer) + interval_id = mapped_column(sa.Integer) interval = relationship( IntervalSchedule, uselist=False, primaryjoin=foreign(interval_id) == remote(IntervalSchedule.id), ) - crontab_id = sa.Column(sa.Integer) + crontab_id = mapped_column(sa.Integer) crontab = relationship( CrontabSchedule, uselist=False, primaryjoin=foreign(crontab_id) == remote(CrontabSchedule.id), ) - solar_id = sa.Column(sa.Integer) + solar_id = mapped_column(sa.Integer) solar = relationship( SolarSchedule, uselist=False, primaryjoin=foreign(solar_id) == remote(SolarSchedule.id), ) - args = sa.Column(sa.Text(), default="[]") - kwargs = sa.Column(sa.Text(), default="{}") + args = mapped_column(sa.Text(), default="[]") + kwargs = mapped_column(sa.Text(), default="{}") # queue for celery - queue = sa.Column(sa.String(255)) + queue = mapped_column(sa.String(255)) # exchange for celery - exchange = sa.Column(sa.String(255)) + exchange = mapped_column(sa.String(255)) # routing_key for celery - routing_key = sa.Column(sa.String(255)) - priority = sa.Column(sa.Integer()) - expires = sa.Column(sa.DateTime(timezone=True)) + routing_key = mapped_column(sa.String(255)) + priority = mapped_column(sa.Integer()) + expires = mapped_column(sa.DateTime(timezone=True)) # 只执行一次 - one_off = sa.Column(sa.Boolean(), default=False) - start_time = sa.Column(sa.DateTime(timezone=True)) - enabled = sa.Column(sa.Boolean(), default=True) - last_run_at = sa.Column(sa.DateTime(timezone=True)) - total_run_count = sa.Column(sa.Integer(), nullable=False, default=0) + one_off = mapped_column(sa.Boolean(), default=False) + start_time = mapped_column(sa.DateTime(timezone=True)) + enabled = mapped_column(sa.Boolean(), default=True) + last_run_at = mapped_column(sa.DateTime(timezone=True)) + total_run_count = mapped_column(sa.Integer(), nullable=False, default=0) # 修改时间 - date_changed = sa.Column( + date_changed = mapped_column( sa.DateTime(timezone=True), default=func.now(), onupdate=func.now() ) - description = sa.Column(sa.Text(), default="") + description = mapped_column(sa.Text(), default="") no_changes = False diff --git a/timApp/document/docentry.py b/timApp/document/docentry.py index 88fde5311e..702dedbfc3 100644 --- a/timApp/document/docentry.py +++ b/timApp/document/docentry.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any from sqlalchemy import select -from sqlalchemy.orm import foreign +from sqlalchemy.orm import foreign, mapped_column from timApp.document.docinfo import DocInfo from timApp.document.document import Document @@ -29,23 +29,23 @@ class DocEntry(db.Model, DocInfo): """ __tablename__ = "docentry" - __allow_unmapped__ = True - name = db.Column(db.Text, primary_key=True) + + name = mapped_column(db.Text, primary_key=True) """Full path of the document. TODO: Improve the name. """ - id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) + id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) """Document identifier.""" - public = db.Column(db.Boolean, nullable=False, default=True) + public = mapped_column(db.Boolean, nullable=False, default=True) """Whether the document is visible in directory listing.""" _block = db.relationship("Block", back_populates="docentries", lazy="joined") - trs: list[Translation] = db.relationship( + trs = db.relationship( "Translation", primaryjoin=id == foreign(Translation.src_docid), back_populates="docentry", @@ -55,7 +55,7 @@ class DocEntry(db.Model, DocInfo): # doesn't sound ideal either. passive_deletes="all", cascade_backrefs=False, - ) + ) # : list[Translation] __table_args__ = (db.Index("docentry_id_idx", "id"),) @@ -155,7 +155,6 @@ def find_by_path( .first() ) if tr is not None: - tr.docentry = entry return tr if fallback_to_id: try: diff --git a/timApp/document/translation/deepl.py b/timApp/document/translation/deepl.py index 101fee63f1..6b8cf7d476 100644 --- a/timApp/document/translation/deepl.py +++ b/timApp/document/translation/deepl.py @@ -19,6 +19,7 @@ from requests import post, Response from requests.exceptions import JSONDecodeError from sqlalchemy import select +from sqlalchemy.orm import mapped_column from timApp.document.translation.language import Language from timApp.document.translation.translationparser import TranslateApproval, NoTranslate @@ -54,23 +55,23 @@ def __init__(self, values: dict): # TODO Would be better as nullable=False, but that prevents creating # non-DeeplTranslationService -subclasses of TranslationService. - service_url = db.Column(db.Text) + service_url = mapped_column(db.Text) """The url base for the API calls.""" # TODO Would be better as nullable=False, but that prevents creating # non-DeeplTranslationService -subclasses of TranslationService. - ignore_tag = db.Column(db.Text) + ignore_tag = mapped_column(db.Text) """The XML-tag name to use for ignoring pieces of text when XML-handling is used. Should be chosen to be some uncommon string not found in many texts. """ - headers: dict[str, str] - """Request-headers needed for authentication with the API-key.""" - - source_Language_code: str - """The source language's code (helps handling regional variants that DeepL - doesn't differentiate). - """ + # headers: ClassVar[dict[str, str]] + # """Request-headers needed for authentication with the API-key.""" + # + # source_Language_code: ClassVar[str] + # """The source language's code (helps handling regional variants that DeepL + # doesn't differentiate). + # """ def register(self, user_group: UserGroup) -> None: """ diff --git a/timApp/document/translation/language.py b/timApp/document/translation/language.py index 876c3100e6..b12b771ffc 100644 --- a/timApp/document/translation/language.py +++ b/timApp/document/translation/language.py @@ -19,6 +19,7 @@ import langcodes from sqlalchemy import select +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db @@ -33,21 +34,22 @@ class Language(db.Model): """ __tablename__ = "language" - __allow_unmapped__ = True + - lang_code = db.Column(db.Text, nullable=False, primary_key=True) + lang_code = mapped_column(db.Text, nullable=False, primary_key=True) """Standardized code of the language.""" # TODO should this be unique? - lang_name = db.Column(db.Text, nullable=False) + lang_name = mapped_column(db.Text, nullable=False) """IANA's name for the language.""" - flag_uri = db.Column(db.Text) + flag_uri = mapped_column(db.Text) """Path to a picture representing the language.""" - autonym = db.Column(db.Text, nullable=False) + autonym = mapped_column(db.Text, nullable=False) """Native name for the language.""" + # FIXME: Turn into postinit def __init__( self, lang_code: str, lang_name: str, autonym: str, flag_uri: str | None = None ): diff --git a/timApp/document/translation/translation.py b/timApp/document/translation/translation.py index fa869c5879..2c39ed0db2 100644 --- a/timApp/document/translation/translation.py +++ b/timApp/document/translation/translation.py @@ -1,4 +1,5 @@ from sqlalchemy import UniqueConstraint +from sqlalchemy.orm import mapped_column from timApp.document.docinfo import DocInfo from timApp.timdb.sqa import db @@ -15,11 +16,11 @@ class Translation(db.Model, DocInfo): """ __tablename__ = "translation" - __allow_unmapped__ = True - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - src_docid = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) - lang_id = db.Column(db.Text, nullable=False) + + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + src_docid = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) + lang_id = mapped_column(db.Text, nullable=False) __table_args__ = (UniqueConstraint("src_docid", "lang_id", name="translation_uc"),) _block = db.relationship( diff --git a/timApp/document/translation/translator.py b/timApp/document/translation/translator.py index f4858ced34..fdbf35a0b8 100644 --- a/timApp/document/translation/translator.py +++ b/timApp/document/translation/translator.py @@ -22,7 +22,7 @@ import pypandoc from sqlalchemy import select -from sqlalchemy.orm import with_polymorphic +from sqlalchemy.orm import with_polymorphic, mapped_column from timApp.document.docparagraph import DocParagraph from timApp.document.translation.language import Language @@ -75,12 +75,12 @@ class TranslationService(db.Model): """ __tablename__ = "translationservice" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) """Translation service identifier.""" - service_name = db.Column(db.Text, unique=True, nullable=False) + service_name = mapped_column(db.Text, unique=True, nullable=False) """Human-readable name of the machine translator. Also used as an identifier.""" @@ -179,25 +179,25 @@ class TranslationServiceKey(db.Model): """ __tablename__ = "translationservicekey" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) """Key identifier.""" # TODO Come up with a better name? - api_key = db.Column(db.Text, nullable=False) + api_key = mapped_column(db.Text, nullable=False) """The key needed for using related service.""" - group_id = db.Column(db.Integer, db.ForeignKey("usergroup.id"), nullable=False) - group: UserGroup = db.relationship("UserGroup", uselist=False) + group_id = mapped_column(db.Integer, db.ForeignKey("usergroup.id"), nullable=False) + group = db.relationship("UserGroup", uselist=False) # : UserGroup """The group that can use this key.""" - service_id = db.Column( + service_id = mapped_column( db.Integer, db.ForeignKey("translationservice.id"), nullable=False, ) - service: TranslationService = db.relationship("TranslationService", uselist=False) + service = db.relationship("TranslationService", uselist=False) # : TranslationService """The service that this key is used in.""" @staticmethod diff --git a/timApp/folder/folder.py b/timApp/folder/folder.py index fc1ddab405..d0c7a8b8ba 100644 --- a/timApp/folder/folder.py +++ b/timApp/folder/folder.py @@ -3,6 +3,7 @@ from typing import Iterable, Any, TYPE_CHECKING from sqlalchemy import true, and_, select, delete +from sqlalchemy.orm import mapped_column from timApp.auth.auth_models import BlockAccess from timApp.document.docentry import DocEntry, get_documents @@ -26,15 +27,15 @@ class Folder(db.Model, Item): """Represents a folder in the directory hierarchy.""" __tablename__ = "folder" - __allow_unmapped__ = True + - id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) """Folder identifier.""" - name = db.Column(db.Text, nullable=False) + name = mapped_column(db.Text, nullable=False) """Folder name (last part of path).""" - location = db.Column(db.Text, nullable=False) + location = mapped_column(db.Text, nullable=False) """Folder location (first parts of the path).""" __table_args__ = (db.UniqueConstraint("name", "location", name="folder_uc"),) diff --git a/timApp/item/block.py b/timApp/item/block.py index e4de9a542d..a457b393b6 100644 --- a/timApp/item/block.py +++ b/timApp/item/block.py @@ -4,17 +4,12 @@ from typing import TYPE_CHECKING from sqlalchemy import func +from sqlalchemy.orm import mapped_column from sqlalchemy.orm.collections import attribute_mapped_collection from timApp.auth.accesstype import AccessType from timApp.auth.auth_models import BlockAccess from timApp.item.blockassociation import BlockAssociation -from timApp.item.tag import Tag -from timApp.messaging.messagelist.messagelist_models import MessageListModel -from timApp.messaging.timMessage.internalmessage_models import ( - InternalMessage, - InternalMessageDisplay, -) from timApp.timdb.sqa import db from timApp.user.usergroup import UserGroup from timApp.user.usergroupdoc import UserGroupDoc @@ -28,30 +23,32 @@ class Block(db.Model): """The "base class" for all database objects that are part of the permission system.""" __tablename__ = "block" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) + + id = mapped_column(db.Integer, primary_key=True) """A unique identifier for the Block.""" - latest_revision_id = db.Column(db.Integer) + latest_revision_id = mapped_column(db.Integer) """Old field that is not used anymore.""" - type_id = db.Column(db.Integer, nullable=False) + type_id = mapped_column(db.Integer, nullable=False) """Type of the Block, see BlockType enum for possible types.""" - description = db.Column(db.Text) + description = mapped_column(db.Text) """Additional information about the Block. This is used for different purposes by different BlockTypes, so it isn't merely a "description". """ - created = db.Column(db.DateTime(timezone=True), nullable=False, default=func.now()) + created = mapped_column(db.DateTime(timezone=True), nullable=False, default=func.now()) """When this Block was created.""" - modified = db.Column(db.DateTime(timezone=True), default=func.now()) + modified = mapped_column(db.DateTime(timezone=True), default=func.now()) """When this Block was last modified.""" docentries = db.relationship("DocEntry", back_populates="_block") - folder = db.relationship("Folder", back_populates="_block", uselist=False, cascade_backrefs=False) + folder = db.relationship( + "Folder", back_populates="_block", uselist=False, cascade_backrefs=False + ) translation = db.relationship( "Translation", back_populates="_block", @@ -70,7 +67,7 @@ class Block(db.Model): collection_class=attribute_mapped_collection("block_collection_key"), cascade_backrefs=False, ) - tags: list[Tag] = db.relationship("Tag", back_populates="block", lazy="select") + tags = db.relationship("Tag", back_populates="block", lazy="select") # : list[Tag] children = db.relationship( "Block", secondary=BlockAssociation.__table__, @@ -95,26 +92,26 @@ class Block(db.Model): ) # If this Block corresponds to a group's manage document, indicates the group being managed. - managed_usergroup: UserGroup | None = db.relationship( + managed_usergroup = db.relationship( "UserGroup", secondary=UserGroupDoc.__table__, lazy="select", uselist=False, overlaps="admin_doc", - ) + ) # : UserGroup | None # If this Block corresponds to a message list's manage document, indicates the message list # being managed. - managed_messagelist: MessageListModel | None = db.relationship( + managed_messagelist = db.relationship( "MessageListModel", back_populates="block", lazy="select" - ) + ) # : MessageListModel | None - internalmessage: InternalMessage | None = db.relationship( + internalmessage = db.relationship( "InternalMessage", back_populates="block", cascade_backrefs=False - ) - internalmessage_display: InternalMessageDisplay | None = db.relationship( + ) # : InternalMessage | None + internalmessage_display = db.relationship( "InternalMessageDisplay", back_populates="display_block", cascade_backrefs=False - ) + ) # : InternalMessageDisplay | None def __json__(self): return ["id", "type_id", "description", "created", "modified"] diff --git a/timApp/item/blockassociation.py b/timApp/item/blockassociation.py index 9156b1376b..77bc4bd276 100644 --- a/timApp/item/blockassociation.py +++ b/timApp/item/blockassociation.py @@ -1,3 +1,5 @@ +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db @@ -5,10 +7,10 @@ class BlockAssociation(db.Model): """Associates blocks with other blocks. Currently only used for associating uploaded files with documents.""" __tablename__ = "blockassociation" - __allow_unmapped__ = True + - parent = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + parent = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) """The parent Block.""" - child = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + child = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) """The child Block.""" diff --git a/timApp/item/blockrelevance.py b/timApp/item/blockrelevance.py index 7a683d0322..084bfbadea 100644 --- a/timApp/item/blockrelevance.py +++ b/timApp/item/blockrelevance.py @@ -1,3 +1,5 @@ +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db @@ -5,9 +7,9 @@ class BlockRelevance(db.Model): """A relevance value of a block (used in search).""" __tablename__ = "blockrelevance" - __allow_unmapped__ = True - block_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - relevance = db.Column(db.Integer, nullable=False) + + block_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + relevance = mapped_column(db.Integer, nullable=False) _block = db.relationship("Block", back_populates="relevance") diff --git a/timApp/item/routes.py b/timApp/item/routes.py index cf8be83f04..93477ff898 100644 --- a/timApp/item/routes.py +++ b/timApp/item/routes.py @@ -17,7 +17,7 @@ from markupsafe import Markup from marshmallow import EXCLUDE from sqlalchemy import select -from sqlalchemy.orm import selectinload, defaultload +from sqlalchemy.orm import defaultload, joinedload from timApp.answer.answers import add_missing_users_from_groups, get_points_by_rule from timApp.auth.accesshelper import ( @@ -521,16 +521,16 @@ def view(item_path: str, route: ViewRoute, render_doc: bool = True) -> FlaskView doc_info = DocEntry.find_by_path( item_path, fallback_to_id=True, - docentry_load_opts=( + docentry_load_opts=[ defaultload(DocEntry._block) .defaultload(Block.accesses) - .selectinload(BlockAccess.usergroup), - selectinload(DocEntry.trs) + .joinedload(BlockAccess.usergroup), + joinedload(DocEntry.trs) # TODO: These selectinloads are for some reason very inefficient at least for certain documents. # See https://github.com/TIM-JYU/TIM/issues/2201. Needs more investigation. # .selectinload(Translation.docentry), # selectinload(DocEntry.trs).selectinload(Translation._block) - ), + ], ) if doc_info is None: return try_return_folder(item_path) diff --git a/timApp/item/tag.py b/timApp/item/tag.py index 5870f65797..44c531c4fb 100644 --- a/timApp/item/tag.py +++ b/timApp/item/tag.py @@ -1,5 +1,7 @@ from enum import Enum, unique +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db @@ -21,12 +23,12 @@ class Tag(db.Model): """A tag with associated document id, tag name, type and expiration date.""" __tablename__ = "tag" - __allow_unmapped__ = True - block_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - name = db.Column(db.Text, primary_key=True) - type = db.Column(db.Enum(TagType), nullable=False) - expires = db.Column(db.DateTime(timezone=True)) + + block_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + name = mapped_column(db.Text, primary_key=True) + type = mapped_column(db.Enum(TagType), nullable=False) + expires = mapped_column(db.DateTime(timezone=True)) block = db.relationship("Block", back_populates="tags") diff --git a/timApp/item/taskblock.py b/timApp/item/taskblock.py index 8fd9f3e20b..23caf2f08a 100644 --- a/timApp/item/taskblock.py +++ b/timApp/item/taskblock.py @@ -1,6 +1,7 @@ from __future__ import annotations from sqlalchemy import select +from sqlalchemy.orm import mapped_column from timApp.item.block import Block, BlockType, insert_block from timApp.timdb.sqa import db @@ -9,10 +10,10 @@ class TaskBlock(db.Model): __tablename__ = "taskblock" - __allow_unmapped__ = True - id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - task_id = db.Column(db.Text, primary_key=True) + + id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + task_id = mapped_column(db.Text, primary_key=True) block = db.relationship("Block", lazy="selectin") diff --git a/timApp/lecture/askedjson.py b/timApp/lecture/askedjson.py index d5c14292e6..ebf4ab13c1 100644 --- a/timApp/lecture/askedjson.py +++ b/timApp/lecture/askedjson.py @@ -3,17 +3,18 @@ from typing import Any from sqlalchemy import select +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db class AskedJson(db.Model): __tablename__ = "askedjson" - __allow_unmapped__ = True - asked_json_id = db.Column(db.Integer, primary_key=True) - json = db.Column(db.Text, nullable=False) - hash = db.Column(db.Text, nullable=False) + + asked_json_id = mapped_column(db.Integer, primary_key=True) + json = mapped_column(db.Text, nullable=False) + hash = mapped_column(db.Text, nullable=False) asked_questions = db.relationship( "AskedQuestion", back_populates="asked_json", lazy="selectin" diff --git a/timApp/lecture/askedquestion.py b/timApp/lecture/askedquestion.py index a415b4bdeb..bd825ab363 100644 --- a/timApp/lecture/askedquestion.py +++ b/timApp/lecture/askedquestion.py @@ -3,9 +3,8 @@ from datetime import timedelta, datetime from sqlalchemy import func, select +from sqlalchemy.orm import mapped_column -from timApp.lecture.askedjson import AskedJson -from timApp.lecture.lecture import Lecture from timApp.lecture.question_utils import qst_rand_array, qst_filter_markup_points from timApp.lecture.questionactivity import QuestionActivityKind, QuestionActivity from timApp.timdb.sqa import db @@ -15,27 +14,27 @@ class AskedQuestion(db.Model): __tablename__ = "askedquestion" - __allow_unmapped__ = True - asked_id = db.Column(db.Integer, primary_key=True) - lecture_id = db.Column( + + asked_id = mapped_column(db.Integer, primary_key=True) + lecture_id = mapped_column( db.Integer, db.ForeignKey("lecture.lecture_id"), nullable=False ) - doc_id = db.Column(db.Integer, db.ForeignKey("block.id")) - par_id = db.Column(db.Text) - asked_time = db.Column(db.DateTime(timezone=True), nullable=False) - points = db.Column(db.Text) # not a single number; cannot be numeric - asked_json_id = db.Column( + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id")) + par_id = mapped_column(db.Text) + asked_time = mapped_column(db.DateTime(timezone=True), nullable=False) + points = mapped_column(db.Text) # not a single number; cannot be numeric + asked_json_id = mapped_column( db.Integer, db.ForeignKey("askedjson.asked_json_id"), nullable=False ) - expl = db.Column(db.Text) + expl = mapped_column(db.Text) - asked_json: AskedJson = db.relationship( + asked_json = db.relationship( "AskedJson", back_populates="asked_questions", lazy="selectin" - ) - lecture: Lecture = db.relationship( + ) # : AskedJson + lecture = db.relationship( "Lecture", back_populates="asked_questions", lazy="selectin" - ) + ) # : Lecture answers = db.relationship( "LectureAnswer", back_populates="asked_question", lazy="dynamic" ) diff --git a/timApp/lecture/lecture.py b/timApp/lecture/lecture.py index 3636c09a7d..7d4809acda 100644 --- a/timApp/lecture/lecture.py +++ b/timApp/lecture/lecture.py @@ -3,6 +3,7 @@ from typing import Optional from sqlalchemy import select, func +from sqlalchemy.orm import mapped_column from timApp.lecture.lectureusers import LectureUsers from timApp.timdb.sqa import db @@ -11,16 +12,18 @@ class Lecture(db.Model): __tablename__ = "lecture" - __allow_unmapped__ = True - lecture_id = db.Column(db.Integer, primary_key=True) - lecture_code = db.Column(db.Text) - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) - lecturer = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) - start_time = db.Column(db.DateTime(timezone=True), nullable=False) - end_time = db.Column(db.DateTime(timezone=True)) - password = db.Column(db.Text) - options = db.Column(db.Text) + + lecture_id = mapped_column(db.Integer, primary_key=True) + lecture_code = mapped_column(db.Text) + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) + lecturer = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), nullable=False + ) + start_time = mapped_column(db.DateTime(timezone=True), nullable=False) + end_time = mapped_column(db.DateTime(timezone=True)) + password = mapped_column(db.Text) + options = mapped_column(db.Text) users = db.relationship( "User", @@ -29,11 +32,17 @@ class Lecture(db.Model): lazy="dynamic", ) asked_questions = db.relationship( - "AskedQuestion", back_populates="lecture", lazy="dynamic", cascade_backrefs=False + "AskedQuestion", + back_populates="lecture", + lazy="dynamic", + cascade_backrefs=False, ) messages = db.relationship("Message", back_populates="lecture", lazy="dynamic") running_questions = db.relationship( - "Runningquestion", back_populates="lecture", lazy="select", cascade_backrefs=False + "Runningquestion", + back_populates="lecture", + lazy="select", + cascade_backrefs=False, ) useractivity = db.relationship( "Useractivity", back_populates="lecture", lazy="select" diff --git a/timApp/lecture/lectureanswer.py b/timApp/lecture/lectureanswer.py index b9a3cec034..71253b3e68 100644 --- a/timApp/lecture/lectureanswer.py +++ b/timApp/lecture/lectureanswer.py @@ -3,7 +3,7 @@ from typing import Optional from sqlalchemy import func, select -from sqlalchemy.orm import lazyload +from sqlalchemy.orm import lazyload, mapped_column from timApp.lecture.lecture import Lecture from timApp.timdb.sqa import db @@ -26,19 +26,19 @@ def unshuffle_lectureanswer( class LectureAnswer(db.Model): __tablename__ = "lectureanswer" - __allow_unmapped__ = True - answer_id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) - question_id = db.Column( + + answer_id = mapped_column(db.Integer, primary_key=True) + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + question_id = mapped_column( db.Integer, db.ForeignKey("askedquestion.asked_id"), nullable=False ) - lecture_id = db.Column( + lecture_id = mapped_column( db.Integer, db.ForeignKey("lecture.lecture_id"), nullable=False ) - answer = db.Column(db.Text, nullable=False) - answered_on = db.Column(db.DateTime(timezone=True), nullable=False) - points = db.Column(db.Float) + answer = mapped_column(db.Text, nullable=False) + answered_on = mapped_column(db.DateTime(timezone=True), nullable=False) + points = mapped_column(db.Float) asked_question = db.relationship( "AskedQuestion", back_populates="answers", lazy="selectin" diff --git a/timApp/lecture/lectureusers.py b/timApp/lecture/lectureusers.py index a8a7cb1d88..cc19d86da0 100644 --- a/timApp/lecture/lectureusers.py +++ b/timApp/lecture/lectureusers.py @@ -1,11 +1,13 @@ +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db class LectureUsers(db.Model): __tablename__ = "lectureusers" - __allow_unmapped__ = True - lecture_id = db.Column( + + lecture_id = mapped_column( db.Integer, db.ForeignKey("lecture.lecture_id"), primary_key=True ) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) diff --git a/timApp/lecture/message.py b/timApp/lecture/message.py index 15b5d9e34e..ad119cd1cb 100644 --- a/timApp/lecture/message.py +++ b/timApp/lecture/message.py @@ -1,19 +1,21 @@ from datetime import datetime +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db class Message(db.Model): __tablename__ = "message" - __allow_unmapped__ = True - msg_id = db.Column(db.Integer, primary_key=True) - lecture_id = db.Column( + + msg_id = mapped_column(db.Integer, primary_key=True) + lecture_id = mapped_column( db.Integer, db.ForeignKey("lecture.lecture_id"), nullable=False ) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) - message = db.Column(db.Text, nullable=False) - timestamp = db.Column( + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + message = mapped_column(db.Text, nullable=False) + timestamp = mapped_column( db.DateTime(timezone=True), nullable=False, default=datetime.utcnow ) diff --git a/timApp/lecture/question.py b/timApp/lecture/question.py index ecc95f030b..4de53ab8b5 100644 --- a/timApp/lecture/question.py +++ b/timApp/lecture/question.py @@ -1,15 +1,17 @@ +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db class Question(db.Model): __tablename__ = "question" - __allow_unmapped__ = True - question_id = db.Column(db.Integer, primary_key=True) - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) - par_id = db.Column(db.Text, nullable=False) - question_title = db.Column(db.Text, nullable=False) - answer = db.Column(db.Text) - questionjson = db.Column(db.Text) - points = db.Column(db.Text) - expl = db.Column(db.Text) + + question_id = mapped_column(db.Integer, primary_key=True) + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) + par_id = mapped_column(db.Text, nullable=False) + question_title = mapped_column(db.Text, nullable=False) + answer = mapped_column(db.Text) + questionjson = mapped_column(db.Text) + points = mapped_column(db.Text) + expl = mapped_column(db.Text) diff --git a/timApp/lecture/questionactivity.py b/timApp/lecture/questionactivity.py index 5b0d1c088e..6ace89dc90 100644 --- a/timApp/lecture/questionactivity.py +++ b/timApp/lecture/questionactivity.py @@ -1,5 +1,7 @@ from enum import Enum +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db @@ -13,13 +15,15 @@ class QuestionActivityKind(Enum): class QuestionActivity(db.Model): __tablename__ = "question_activity" - __allow_unmapped__ = True - asked_id = db.Column( + + asked_id = mapped_column( db.Integer, db.ForeignKey("askedquestion.asked_id"), primary_key=True ) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) - kind = db.Column(db.Enum(QuestionActivityKind), primary_key=True) + user_id = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + ) + kind = mapped_column(db.Enum(QuestionActivityKind), primary_key=True) asked_question = db.relationship( "AskedQuestion", back_populates="questionactivity", lazy="select" diff --git a/timApp/lecture/runningquestion.py b/timApp/lecture/runningquestion.py index a2a29d6743..b60c247b06 100644 --- a/timApp/lecture/runningquestion.py +++ b/timApp/lecture/runningquestion.py @@ -1,26 +1,27 @@ from datetime import datetime -from timApp.lecture.askedquestion import AskedQuestion +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db class Runningquestion(db.Model): - __allow_unmapped__ = True - asked_id = db.Column( + + asked_id = mapped_column( db.Integer, db.ForeignKey("askedquestion.asked_id"), primary_key=True ) - lecture_id = db.Column( + lecture_id = mapped_column( db.Integer, db.ForeignKey("lecture.lecture_id"), primary_key=True ) # TODO should not be part of primary key (asked_id is enough) - ask_time = db.Column( + ask_time = mapped_column( db.DateTime(timezone=True), nullable=False, default=datetime.utcnow ) - end_time = db.Column(db.DateTime(timezone=True)) + end_time = mapped_column(db.DateTime(timezone=True)) - asked_question: AskedQuestion = db.relationship( + asked_question = db.relationship( "AskedQuestion", back_populates="running_question", lazy="select" - ) + ) # : AskedQuestion lecture = db.relationship( "Lecture", back_populates="running_questions", lazy="select" ) diff --git a/timApp/lecture/showpoints.py b/timApp/lecture/showpoints.py index 811687ff6e..ddc6c92180 100644 --- a/timApp/lecture/showpoints.py +++ b/timApp/lecture/showpoints.py @@ -1,11 +1,13 @@ +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db class Showpoints(db.Model): __tablename__ = "showpoints" - __allow_unmapped__ = True - asked_id = db.Column( + + asked_id = mapped_column( db.Integer, db.ForeignKey("askedquestion.asked_id"), primary_key=True ) diff --git a/timApp/lecture/useractivity.py b/timApp/lecture/useractivity.py index 1150150079..72fea2da2e 100644 --- a/timApp/lecture/useractivity.py +++ b/timApp/lecture/useractivity.py @@ -1,17 +1,22 @@ from sqlalchemy import func +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db class Useractivity(db.Model): __tablename__ = "useractivity" - __allow_unmapped__ = True - lecture_id = db.Column( + + lecture_id = mapped_column( db.Integer, db.ForeignKey("lecture.lecture_id"), primary_key=True ) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) - active = db.Column(db.DateTime(timezone=True), nullable=False, default=func.now()) + user_id = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + ) + active = mapped_column( + db.DateTime(timezone=True), nullable=False, default=func.now() + ) user = db.relationship("User", back_populates="useractivity", lazy="select") lecture = db.relationship("Lecture", back_populates="useractivity", lazy="select") diff --git a/timApp/messaging/messagelist/messagelist_models.py b/timApp/messaging/messagelist/messagelist_models.py index fe283f5e75..5bab9e5550 100644 --- a/timApp/messaging/messagelist/messagelist_models.py +++ b/timApp/messaging/messagelist/messagelist_models.py @@ -4,6 +4,7 @@ from sqlalchemy import select from sqlalchemy.ext.hybrid import hybrid_property # type: ignore +from sqlalchemy.orm import mapped_column from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound # type: ignore from timApp.messaging.messagelist.listinfo import ( @@ -34,65 +35,65 @@ class MessageListModel(db.Model): """Database model for message lists""" __tablename__ = "messagelist" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) - manage_doc_id = db.Column(db.Integer, db.ForeignKey("block.id")) + manage_doc_id = mapped_column(db.Integer, db.ForeignKey("block.id")) """The document which manages a message list.""" - name = db.Column(db.Text) + name = mapped_column(db.Text) """The name of a message list.""" - can_unsubscribe = db.Column(db.Boolean) + can_unsubscribe = mapped_column(db.Boolean) """If a member can unsubscribe from this list on their own.""" - email_list_domain = db.Column(db.Text) + email_list_domain = mapped_column(db.Text) """The domain used for an email list attached to a message list. If None/null, then message list doesn't have an attached email list. This is a tad silly at this point in time, because JYU TIM only has one domain. However, this allows quick adaptation if more domains are added or otherwise changed in the future. """ - archive = db.Column(db.Enum(ArchiveType)) + archive = mapped_column(db.Enum(ArchiveType)) """The archive policy of a message list.""" - notify_owner_on_change = db.Column(db.Boolean) + notify_owner_on_change = mapped_column(db.Boolean) """Should the owner of the message list be notified if there are changes on message list members.""" - description = db.Column(db.Text) + description = mapped_column(db.Text) """A short description what a message list is about.""" - info = db.Column(db.Text) + info = mapped_column(db.Text) """Additional information about the message list.""" - removed = db.Column(db.DateTime(timezone=True)) + removed = mapped_column(db.DateTime(timezone=True)) """When this list has been marked for removal.""" - default_send_right = db.Column(db.Boolean) + default_send_right = mapped_column(db.Boolean) """Default send right for new members who join the list on their own.""" - default_delivery_right = db.Column(db.Boolean) + default_delivery_right = mapped_column(db.Boolean) """Default delivery right for new members who join the list on their own.""" - tim_user_can_join = db.Column(db.Boolean) + tim_user_can_join = mapped_column(db.Boolean) """Flag if TIM users can join the list on their own.""" - subject_prefix = db.Column(db.Text) + subject_prefix = mapped_column(db.Text) """What prefix message subjects that go through the list get.""" - only_text = db.Column(db.Boolean) + only_text = mapped_column(db.Boolean) """Flag if only text format messages are allowed on a list.""" - default_reply_type = db.Column(db.Enum(ReplyToListChanges)) + default_reply_type = mapped_column(db.Enum(ReplyToListChanges)) """Default reply type for the list.""" - non_member_message_pass = db.Column(db.Boolean) + non_member_message_pass = mapped_column(db.Boolean) """Flag if non members messages to the list are passed straight through without moderation.""" - allow_attachments = db.Column(db.Boolean) + allow_attachments = mapped_column(db.Boolean) """Flag if attachments are allowed on the list. The list of allowed attachment file extensions are stored at listoptions.py """ - message_verification = db.Column( + message_verification = mapped_column( db.Enum(MessageVerificationType), nullable=False, default=MessageVerificationType.MUNGE_FROM, @@ -104,9 +105,9 @@ class MessageListModel(db.Model): ) """Relationship to the document that is used to manage this message list.""" - members: list["MessageListTimMember"] = db.relationship( + members = db.relationship( "MessageListMember", back_populates="message_list", lazy="select" - ) + ) # : list["MessageListTimMember"] """All the members of the list.""" distribution = db.relationship( @@ -266,32 +267,32 @@ class MessageListMember(db.Model): """Database model for members of a message list.""" __tablename__ = "messagelist_member" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) - message_list_id = db.Column(db.Integer, db.ForeignKey("messagelist.id")) + message_list_id = mapped_column(db.Integer, db.ForeignKey("messagelist.id")) """What message list a member belongs to.""" - send_right = db.Column(db.Boolean) + send_right = mapped_column(db.Boolean) """If a member can send messages to a message list.""" - delivery_right = db.Column(db.Boolean) + delivery_right = mapped_column(db.Boolean) """If a member can get messages from a message list.""" - membership_ended = db.Column(db.DateTime(timezone=True)) + membership_ended = mapped_column(db.DateTime(timezone=True)) """When member's membership on a list ended. This is set when member is removed from a list. A value of None means the member is still on the list.""" - join_method = db.Column(db.Enum(MemberJoinMethod)) + join_method = mapped_column(db.Enum(MemberJoinMethod)) """How the member came to a list.""" - membership_verified = db.Column(db.DateTime(timezone=True)) + membership_verified = mapped_column(db.DateTime(timezone=True)) """When the user's joining was verified. If user is added e.g. by a teacher to a course's message list, this date is the date teacher added the member. If the member was invited, then this is the date they verified their join. """ - member_type = db.Column(db.Text) + member_type = mapped_column(db.Text) """Discriminator for polymorhphic members.""" message_list = db.relationship( @@ -402,9 +403,9 @@ class MessageListTimMember(MessageListMember): __tablename__ = "messagelist_tim_member" - id = db.Column(db.Integer, db.ForeignKey("messagelist_member.id"), primary_key=True) + id = mapped_column(db.Integer, db.ForeignKey("messagelist_member.id"), primary_key=True) - group_id = db.Column(db.Integer, db.ForeignKey("usergroup.id")) + group_id = mapped_column(db.Integer, db.ForeignKey("usergroup.id")) """A UserGroup id for a member.""" member = db.relationship( @@ -461,12 +462,12 @@ class MessageListExternalMember(MessageListMember): __tablename__ = "messagelist_external_member" - id = db.Column(db.Integer, db.ForeignKey("messagelist_member.id"), primary_key=True) + id = mapped_column(db.Integer, db.ForeignKey("messagelist_member.id"), primary_key=True) - email_address = db.Column(db.Text) + email_address = mapped_column(db.Text) """Email address of message list's external member.""" - display_name = db.Column(db.Text) + display_name = mapped_column(db.Text) """Display name for external user, which in most cases should be the external member's address' owner's name.""" member = db.relationship( @@ -508,17 +509,17 @@ class MessageListDistribution(db.Model): """Message list member's chosen distribution channels.""" __tablename__ = "messagelist_distribution" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("messagelist_member.id")) + user_id = mapped_column(db.Integer, db.ForeignKey("messagelist_member.id")) """Message list member's id, if this row is about message list member's channel distribution.""" - message_list_id = db.Column(db.Integer, db.ForeignKey("messagelist.id")) + message_list_id = mapped_column(db.Integer, db.ForeignKey("messagelist.id")) """Message list's id, if this row is about message list's channel distribution.""" - channel = db.Column(db.Enum(Channel)) + channel = mapped_column(db.Enum(Channel)) """Which message channels are used by a message list or a user.""" member = db.relationship( diff --git a/timApp/messaging/timMessage/internalmessage_models.py b/timApp/messaging/timMessage/internalmessage_models.py index 3ea715c079..f65f6624e8 100644 --- a/timApp/messaging/timMessage/internalmessage_models.py +++ b/timApp/messaging/timMessage/internalmessage_models.py @@ -2,6 +2,7 @@ from typing import Any, Optional, TYPE_CHECKING from sqlalchemy import func, select +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db @@ -18,39 +19,39 @@ class InternalMessage(db.Model): """A TIM message.""" __tablename__ = "internalmessage" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) """Message identifier.""" - created = db.Column(db.DateTime(timezone=True), nullable=False, default=func.now()) + created = mapped_column(db.DateTime(timezone=True), nullable=False, default=func.now()) """Date and time when the message was created.""" - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) """Block identifier.""" - par_id = db.Column(db.Text, nullable=False) + par_id = mapped_column(db.Text, nullable=False) """Paragraph identifier.""" - can_mark_as_read = db.Column(db.Boolean, nullable=False) + can_mark_as_read = mapped_column(db.Boolean, nullable=False) """Whether the recipient can mark the message as read.""" - reply = db.Column(db.Boolean, nullable=False) + reply = mapped_column(db.Boolean, nullable=False) """Whether the message can be replied to.""" - display_type = db.Column(db.Enum(DisplayType), nullable=False) + display_type = mapped_column(db.Enum(DisplayType), nullable=False) """How the message is displayed.""" - expires = db.Column(db.DateTime) + expires = mapped_column(db.DateTime) """"When the message display will disappear.""" - replies_to = db.Column(db.Integer) + replies_to = mapped_column(db.Integer) """Id of the message which this messages is a reply to""" displays = db.relationship("InternalMessageDisplay", back_populates="message") - readreceipts: list["InternalMessageReadReceipt"] = db.relationship( + readreceipts = db.relationship( "InternalMessageReadReceipt", back_populates="message" - ) + ) # : list["InternalMessageReadReceipt"] block = db.relationship("Block", back_populates="internalmessage") def to_json(self) -> dict[str, Any]: @@ -71,20 +72,20 @@ class InternalMessageDisplay(db.Model): """Where and for whom a TIM message is displayed.""" __tablename__ = "internalmessage_display" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) """Message display identifier.""" - message_id = db.Column( + message_id = mapped_column( db.Integer, db.ForeignKey("internalmessage.id"), nullable=False ) """Message identifier.""" - usergroup_id = db.Column(db.Integer, db.ForeignKey("usergroup.id")) + usergroup_id = mapped_column(db.Integer, db.ForeignKey("usergroup.id")) """Who sees the message; if null, displayed for everyone.""" - display_doc_id = db.Column(db.Integer, db.ForeignKey("block.id")) + display_doc_id = mapped_column(db.Integer, db.ForeignKey("block.id")) """ Identifier for the document or the folder where the message is displayed. If null, the message is displayed globally. @@ -107,20 +108,20 @@ class InternalMessageReadReceipt(db.Model): """Metadata about read receipts.""" __tablename__ = "internalmessage_readreceipt" - __allow_unmapped__ = True + - message_id = db.Column( + message_id = mapped_column( db.Integer, db.ForeignKey("internalmessage.id"), primary_key=True ) """Message identifier.""" - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) """Identifier for the user who marked the message as read.""" - last_seen = db.Column(db.DateTime) + last_seen = mapped_column(db.DateTime) """Timestamp for the last time the the message was displayed to the user""" - marked_as_read_on = db.Column(db.DateTime) + marked_as_read_on = mapped_column(db.DateTime) """Timestamp for when the message was marked as read.""" message = db.relationship("InternalMessage", back_populates="readreceipts") diff --git a/timApp/note/usernote.py b/timApp/note/usernote.py index 3502223a02..9cca12121d 100644 --- a/timApp/note/usernote.py +++ b/timApp/note/usernote.py @@ -1,4 +1,5 @@ from sqlalchemy import func +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db @@ -7,39 +8,43 @@ class UserNote(db.Model): """A comment/note that has been posted in a document paragraph.""" __tablename__ = "usernotes" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) + + id = mapped_column(db.Integer, primary_key=True) """Comment id.""" - usergroup_id = db.Column(db.Integer, db.ForeignKey("usergroup.id"), nullable=False) + usergroup_id = mapped_column( + db.Integer, db.ForeignKey("usergroup.id"), nullable=False + ) """The UserGroup id who posted the comment.""" - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) """The document id in which this comment was posted.""" - par_id = db.Column(db.Text, nullable=False) + par_id = mapped_column(db.Text, nullable=False) """The paragraph id in which this comment was posted.""" - par_hash = db.Column(db.Text, nullable=False) + par_hash = mapped_column(db.Text, nullable=False) """The paragraph hash at the time this comment was posted.""" - content = db.Column(db.Text, nullable=False) + content = mapped_column(db.Text, nullable=False) """Comment content.""" - created = db.Column(db.DateTime(timezone=True), nullable=False, default=func.now()) + created = mapped_column( + db.DateTime(timezone=True), nullable=False, default=func.now() + ) """Comment creation timestamp.""" - modified = db.Column(db.DateTime(timezone=True)) + modified = mapped_column(db.DateTime(timezone=True)) """Comment modification timestamp.""" - access = db.Column(db.Text, nullable=False) + access = mapped_column(db.Text, nullable=False) """Who can see this comment. So far valid values are 'everyone' and 'justme'.""" - tags = db.Column(db.Text, nullable=False) + tags = mapped_column(db.Text, nullable=False) """Tags for the comment.""" - html = db.Column(db.Text) + html = mapped_column(db.Text) """Comment HTML cache.""" usergroup = db.relationship("UserGroup", back_populates="notes") diff --git a/timApp/notification/notification.py b/timApp/notification/notification.py index a56a0296f2..ef655834f8 100644 --- a/timApp/notification/notification.py +++ b/timApp/notification/notification.py @@ -1,6 +1,8 @@ import enum -from timApp.item.block import Block, BlockType +from sqlalchemy.orm import mapped_column + +from timApp.item.block import BlockType from timApp.timdb.sqa import db, is_attribute_loaded from timApp.util.logger import log_warning @@ -29,19 +31,21 @@ class Notification(db.Model): """Notification settings for a User for a block.""" __tablename__ = "notification" - __allow_unmapped__ = True - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) + + user_id = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + ) """User id.""" - block_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + block_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) """Item id.""" - notification_type = db.Column(db.Enum(NotificationType), primary_key=True) + notification_type = mapped_column(db.Enum(NotificationType), primary_key=True) """Notification type.""" user = db.relationship("User", back_populates="notifications") - block: Block = db.relationship("Block", back_populates="notifications") + block = db.relationship("Block", back_populates="notifications") # : Block def to_json(self) -> dict: j = {"type": self.notification_type} diff --git a/timApp/notification/pending_notification.py b/timApp/notification/pending_notification.py index 7935d93b0d..6e9e2269fb 100644 --- a/timApp/notification/pending_notification.py +++ b/timApp/notification/pending_notification.py @@ -1,28 +1,28 @@ from sqlalchemy import func, select +from sqlalchemy.orm import mapped_column from timApp.document.version import Version from timApp.notification.notification import NotificationType from timApp.timdb.sqa import db -from timApp.user.user import User GroupingKey = tuple[int, str] class PendingNotification(db.Model): __tablename__ = "pendingnotification" - __allow_unmapped__ = True - - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) - discriminant = db.Column(db.Text, nullable=False) - par_id = db.Column(db.Text, nullable=True) - text = db.Column(db.Text, nullable=True) - created = db.Column(db.DateTime(timezone=True), nullable=False, default=func.now()) - processed = db.Column(db.DateTime(timezone=True), nullable=True, index=True) - kind = db.Column(db.Enum(NotificationType), nullable=False) - - user: User = db.relationship("User", lazy="selectin") + + + id = mapped_column(db.Integer, primary_key=True) + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) + discriminant = mapped_column(db.Text, nullable=False) + par_id = mapped_column(db.Text, nullable=True) + text = mapped_column(db.Text, nullable=True) + created = mapped_column(db.DateTime(timezone=True), nullable=False, default=func.now()) + processed = mapped_column(db.DateTime(timezone=True), nullable=True, index=True) + kind = mapped_column(db.Enum(NotificationType), nullable=False) + + user = db.relationship("User", lazy="selectin") # : User block = db.relationship("Block") @property @@ -39,7 +39,7 @@ def notify_type(self) -> NotificationType: class DocumentNotification(PendingNotification): """A notification that a document has changed.""" - version_change: str = db.Column(db.Text) # like "1,2/1,3" + version_change = mapped_column(db.Text) # : str # like "1,2/1,3" @property def version_before(self) -> Version: @@ -75,8 +75,8 @@ def grouping_key(self) -> GroupingKey: class AnswerNotification(PendingNotification): """A notification that an answer has been added, changed or deleted.""" - answer_number = db.Column(db.Integer) - task_id = db.Column(db.Text) + answer_number = mapped_column(db.Integer) + task_id = mapped_column(db.Text) @property def grouping_key(self) -> GroupingKey: diff --git a/timApp/peerreview/peerreview.py b/timApp/peerreview/peerreview.py index 00558bc6b3..5cdae42c0d 100644 --- a/timApp/peerreview/peerreview.py +++ b/timApp/peerreview/peerreview.py @@ -1,5 +1,7 @@ from typing import Any +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db @@ -7,41 +9,41 @@ class PeerReview(db.Model): """A peer review to a task.""" __tablename__ = "peer_review" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) + + id = mapped_column(db.Integer, primary_key=True) """Review identifier.""" - answer_id = db.Column(db.Integer, db.ForeignKey("answer.id"), nullable=True) + answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id"), nullable=True) """Answer id.""" - task_name = db.Column(db.Text, nullable=True) + task_name = mapped_column(db.Text, nullable=True) """Task name""" - block_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) + block_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) """Doc id""" - reviewer_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + reviewer_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) """Reviewer id""" - reviewable_id = db.Column( + reviewable_id = mapped_column( db.Integer, db.ForeignKey("useraccount.id"), nullable=False ) """Reviewable id""" - start_time = db.Column(db.DateTime(timezone=True), nullable=False) + start_time = mapped_column(db.DateTime(timezone=True), nullable=False) """Review start time""" - end_time = db.Column(db.DateTime(timezone=True), nullable=False) + end_time = mapped_column(db.DateTime(timezone=True), nullable=False) """Review end time""" - reviewed = db.Column(db.Boolean, default=False) + reviewed = mapped_column(db.Boolean, default=False) """Review status""" - points = db.Column(db.Float) + points = mapped_column(db.Float) """Points given by the reviewer""" - comment = db.Column(db.Text) + comment = mapped_column(db.Text) """Review comment""" __table_args__ = ( diff --git a/timApp/plugin/calendar/models.py b/timApp/plugin/calendar/models.py index 4a9d4c3aca..cf1ed5abbc 100644 --- a/timApp/plugin/calendar/models.py +++ b/timApp/plugin/calendar/models.py @@ -16,6 +16,7 @@ from typing import Optional, Iterable from sqlalchemy import func, select +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db from timApp.user.user import User @@ -27,20 +28,20 @@ class EventGroup(db.Model): """Information about a user group participating in an event.""" __tablename__ = "eventgroup" - __allow_unmapped__ = True + - event_id = db.Column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) + event_id = mapped_column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) """Event the the group belongs to""" - usergroup_id = db.Column( + usergroup_id = mapped_column( db.Integer, db.ForeignKey("usergroup.id"), primary_key=True ) """The usergroup that belongs to the group""" - manager = db.Column(db.Boolean) + manager = mapped_column(db.Boolean) """Is the group a manager (i.e. is able to modify event settings)?""" - extra = db.Column(db.Boolean, nullable=False, default=False) + extra = mapped_column(db.Boolean, nullable=False, default=False) """Is this group an extra group (i.e. can it enroll without affecting the event capacity)?""" user_group = db.relationship(UserGroup, lazy="select") @@ -51,20 +52,20 @@ class Enrollment(db.Model): """A single enrollment in an event""" __tablename__ = "enrollment" - __allow_unmapped__ = True + - event_id = db.Column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) + event_id = mapped_column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) """Event the enrollment is for""" - usergroup_id = db.Column( + usergroup_id = mapped_column( db.Integer, db.ForeignKey("usergroup.id"), primary_key=True ) """The usergroup that is enrolled (i.e. booked) in the event""" - booker_message = db.Column(db.Text) + booker_message = mapped_column(db.Text) """The message left by the booker""" - enroll_type_id = db.Column( + enroll_type_id = mapped_column( db.Integer, db.ForeignKey("enrollmenttype.enroll_type_id"), nullable=False ) """Type of the enrollment""" @@ -75,7 +76,7 @@ class Enrollment(db.Model): usergroup = db.relationship(UserGroup, lazy="select") """User group that booked the event""" - extra = db.Column(db.Boolean, nullable=False, default=False) + extra = mapped_column(db.Boolean, nullable=False, default=False) """Is this an extra enrollment (i.e. can it enroll without affecting the event capacity)?""" @staticmethod @@ -99,11 +100,11 @@ class EventTagAttachment(db.Model): """Attachment information for the event tag""" __tablename__ = "eventtagattachment" - __allow_unmapped__ = True + - event_id = db.Column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) + event_id = mapped_column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) """Event the tag is attached to""" - tag_id = db.Column(db.Integer, db.ForeignKey("eventtag.tag_id"), primary_key=True) + tag_id = mapped_column(db.Integer, db.ForeignKey("eventtag.tag_id"), primary_key=True) """Tag that is attached to the event""" @@ -111,20 +112,20 @@ class EventTag(db.Model): """A string tag that can be attached to an event""" __tablename__ = "eventtag" - __allow_unmapped__ = True + - tag_id = db.Column(db.Integer, primary_key=True) + tag_id = mapped_column(db.Integer, primary_key=True) """The id of the tag""" - tag = db.Column(db.Text, nullable=False) + tag = mapped_column(db.Text, nullable=False) """The tag itself""" - events: list["Event"] = db.relationship( + events = db.relationship( "Event", secondary=EventTagAttachment.__table__, lazy="select", back_populates="tags", - ) + ) # : list["Event"] @staticmethod def get_or_create(tags: Iterable[str]) -> list["EventTag"]: @@ -180,82 +181,82 @@ class Event(db.Model): """A calendar event. Event has metadata (title, time, location) and various participating user groups.""" __tablename__ = "event" - __allow_unmapped__ = True + - event_id = db.Column(db.Integer, primary_key=True) + event_id = mapped_column(db.Integer, primary_key=True) """Identification number of the event""" - location = db.Column(db.Text) + location = mapped_column(db.Text) """Location of the event""" - max_size = db.Column(db.Integer) + max_size = mapped_column(db.Integer) """How many people can attend the event""" - start_time = db.Column(db.DateTime(timezone=True), nullable=False) + start_time = mapped_column(db.DateTime(timezone=True), nullable=False) """Start time of the event""" - end_time = db.Column(db.DateTime(timezone=True), nullable=False) + end_time = mapped_column(db.DateTime(timezone=True), nullable=False) """End time of the event""" - message = db.Column(db.Text) + message = mapped_column(db.Text) """Message visible to anyone who can see the event""" - title = db.Column(db.Text, nullable=False) + title = mapped_column(db.Text, nullable=False) """Title of the event""" - signup_before = db.Column(db.DateTime(timezone=True)) + signup_before = mapped_column(db.DateTime(timezone=True)) """Time until signup is closed""" - creator_user_id = db.Column( + creator_user_id = mapped_column( db.Integer, db.ForeignKey("useraccount.id"), nullable=False ) """User who created the event originally""" - origin_doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=True) + origin_doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=True) """Document that was used to create the event""" origin_doc = db.relationship("Block", lazy="select") """Document that was used to create the event""" - enrolled_users: list[UserGroup] = db.relationship( + enrolled_users = db.relationship( UserGroup, Enrollment.__table__, primaryjoin=event_id == Enrollment.event_id, lazy="select", overlaps="event, usergroup", - ) + ) # : list[UserGroup] """List of usergroups that are enrolled in the event""" - enrollments: list[Enrollment] = db.relationship( + enrollments = db.relationship( Enrollment, lazy="select", back_populates="event", cascade="all, delete-orphan", overlaps="enrolled_users", - ) + ) # : list[Enrollment] """Enrollment information for the event""" - creator: User = db.relationship(User) + creator = db.relationship(User) # : User """User who created the event originally""" - send_notifications = db.Column(db.Boolean, nullable=False, default=True) + send_notifications = mapped_column(db.Boolean, nullable=False, default=True) """Whether to send notifications related to enrollment to the event""" - important = db.Column(db.Boolean, nullable=False, default=False) + important = mapped_column(db.Boolean, nullable=False, default=False) """Whether the event is important (i.e. should be show as special in calendar)""" - event_groups: list[EventGroup] = db.relationship( + event_groups = db.relationship( EventGroup, foreign_keys="EventGroup.event_id", cascade="all,delete-orphan", - ) + ) # : list[EventGroup] - tags: list[EventTag] = db.relationship( + tags = db.relationship( EventTag, secondary=EventTagAttachment.__table__, lazy="select", back_populates="events", - ) + ) # : list[EventTag] """Tags attached to the event""" @property @@ -422,12 +423,12 @@ class EnrollmentType(db.Model): """Table for enrollment type, combines enrollment type ID to specific enrollment type""" __tablename__ = "enrollmenttype" - __allow_unmapped__ = True + - enroll_type_id = db.Column(db.Integer, primary_key=True) + enroll_type_id = mapped_column(db.Integer, primary_key=True) """Enrollment type""" - enroll_type = db.Column(db.Text, nullable=False) + enroll_type = mapped_column(db.Text, nullable=False) """Name of the enrollment type""" @@ -435,14 +436,14 @@ class ExportedCalendar(db.Model): """Information about exported calendars""" __tablename__ = "exportedcalendar" - __allow_unmapped__ = True - user_id = db.Column( + + user_id = mapped_column( db.Integer, db.ForeignKey("useraccount.id"), primary_key=True, nullable=False ) """User who created the exported calendar""" - calendar_hash = db.Column(db.Text, nullable=False) + calendar_hash = mapped_column(db.Text, nullable=False) """Hash of the exported calendar""" user = db.relationship(User) diff --git a/timApp/plugin/plugintype.py b/timApp/plugin/plugintype.py index 6e06c657a4..abecba7ac0 100644 --- a/timApp/plugin/plugintype.py +++ b/timApp/plugin/plugintype.py @@ -4,6 +4,7 @@ import filelock from sqlalchemy import select from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import mapped_column import timApp from timApp.timdb.sqa import db @@ -35,11 +36,11 @@ def to_json(self) -> dict[str, Any]: # TODO: Right now values are added dynamically to the table when saving answers. Instead add them on TIM start. class PluginType(db.Model, PluginTypeBase): __tablename__ = "plugintype" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) + + id = mapped_column(db.Integer, primary_key=True) - type = db.Column(db.Text, nullable=False, unique=True) + type = mapped_column(db.Text, nullable=False, unique=True) @staticmethod def resolve(p_type: str) -> "PluginType": diff --git a/timApp/plugin/timtable/row_owner_info.py b/timApp/plugin/timtable/row_owner_info.py index 9c0e5306cc..a72436f40e 100644 --- a/timApp/plugin/timtable/row_owner_info.py +++ b/timApp/plugin/timtable/row_owner_info.py @@ -1,3 +1,5 @@ +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db @@ -8,12 +10,12 @@ class RowOwnerInfo(db.Model): """ __tablename__ = "rowownerinfo" - __allow_unmapped__ = True - doc_id = db.Column(db.Integer, primary_key=True) - par_id = db.Column(db.Text, primary_key=True) - unique_row_id = db.Column(db.Integer, primary_key=True) - usergroup_id = db.Column( + + doc_id = mapped_column(db.Integer, primary_key=True) + par_id = mapped_column(db.Text, primary_key=True) + unique_row_id = mapped_column(db.Integer, primary_key=True) + usergroup_id = mapped_column( db.Integer, db.ForeignKey("usergroup.id"), primary_key=False ) diff --git a/timApp/printing/printeddoc.py b/timApp/printing/printeddoc.py index 2902acd799..88c3905e98 100644 --- a/timApp/printing/printeddoc.py +++ b/timApp/printing/printeddoc.py @@ -1,4 +1,5 @@ from sqlalchemy import func +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db @@ -8,28 +9,32 @@ class PrintedDoc(db.Model): (CSS printing does not count because it happens entirely in browser).""" __tablename__ = "printed_doc" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=False) + + id = mapped_column(db.Integer, primary_key=True) + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) """Id of the printed document.""" - template_doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), nullable=True) + template_doc_id = mapped_column( + db.Integer, db.ForeignKey("block.id"), nullable=True + ) """Id of the template document.""" - file_type = db.Column(db.Text, nullable=False) + file_type = mapped_column(db.Text, nullable=False) """The filetype of the print.""" - path_to_file = db.Column(db.Text, nullable=True) + path_to_file = mapped_column(db.Text, nullable=True) """Path to the printed document in the filesystem.""" - version = db.Column(db.Text, nullable=False) + version = mapped_column(db.Text, nullable=False) """Version (in practice, a hash) for identifying whether a document has already been printed and can be fetched from cache. """ - temp = db.Column(db.Boolean, default=True, nullable=False) + temp = mapped_column(db.Boolean, default=True, nullable=False) """Whether the printed document is stored only temporarily (gets deleted after some time).""" - created = db.Column(db.DateTime(timezone=True), default=func.now(), nullable=False) + created = mapped_column( + db.DateTime(timezone=True), default=func.now(), nullable=False + ) """Timestamp of printing.""" diff --git a/timApp/readmark/readparagraph.py b/timApp/readmark/readparagraph.py index cc9864fc2a..df97a4b6ce 100644 --- a/timApp/readmark/readparagraph.py +++ b/timApp/readmark/readparagraph.py @@ -1,4 +1,5 @@ from sqlalchemy import func +from sqlalchemy.orm import mapped_column from timApp.readmark.readparagraphtype import ReadParagraphType from timApp.timdb.sqa import db @@ -8,27 +9,29 @@ class ReadParagraph(db.Model): """Denotes that a User(Group) has read a specific paragraph in some way.""" __tablename__ = "readparagraph" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) + + id = mapped_column(db.Integer, primary_key=True) """Readmark id.""" - usergroup_id = db.Column(db.Integer, db.ForeignKey("usergroup.id"), nullable=False) + usergroup_id = mapped_column( + db.Integer, db.ForeignKey("usergroup.id"), nullable=False + ) """UserGroup id.""" - doc_id = db.Column(db.Integer, db.ForeignKey("block.id")) + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id")) """Document id.""" - par_id = db.Column(db.Text, nullable=False) + par_id = mapped_column(db.Text, nullable=False) """Paragraph id.""" - type = db.Column(db.Enum(ReadParagraphType), nullable=False) + type = mapped_column(db.Enum(ReadParagraphType), nullable=False) """Readmark type.""" - par_hash = db.Column(db.Text, nullable=False) + par_hash = mapped_column(db.Text, nullable=False) """Paragraph hash at the time the readmark was registered.""" - timestamp = db.Column( + timestamp = mapped_column( db.DateTime(timezone=True), nullable=False, default=func.now() ) """The time the readmark was registered.""" @@ -39,5 +42,3 @@ class ReadParagraph(db.Model): ) usergroup = db.relationship("UserGroup", back_populates="readparagraphs") - - diff --git a/timApp/sisu/scimusergroup.py b/timApp/sisu/scimusergroup.py index 323a01d8c0..5ac65e3538 100644 --- a/timApp/sisu/scimusergroup.py +++ b/timApp/sisu/scimusergroup.py @@ -1,5 +1,7 @@ import re +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db uuid_re = "[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}" @@ -10,10 +12,10 @@ class ScimUserGroup(db.Model): __tablename__ = "scimusergroup" - __allow_unmapped__ = True - group_id = db.Column(db.Integer, db.ForeignKey("usergroup.id"), primary_key=True) - external_id = db.Column(db.Text, unique=True, nullable=False) + + group_id = mapped_column(db.Integer, db.ForeignKey("usergroup.id"), primary_key=True) + external_id = mapped_column(db.Text, unique=True, nullable=False) @property def is_studysubgroup(self) -> bool: diff --git a/timApp/slide/slidestatus.py b/timApp/slide/slidestatus.py index 520e917ec7..7793f6867c 100644 --- a/timApp/slide/slidestatus.py +++ b/timApp/slide/slidestatus.py @@ -1,12 +1,14 @@ +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db class SlideStatus(db.Model): __tablename__ = "slide_status" - __allow_unmapped__ = True - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - status = db.Column(db.Text, nullable=False) + + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + status = mapped_column(db.Text, nullable=False) def __init__(self, doc_id, status): self.doc_id = doc_id diff --git a/timApp/tim.py b/timApp/tim.py index e10ec1e007..4d40682f18 100755 --- a/timApp/tim.py +++ b/timApp/tim.py @@ -272,24 +272,25 @@ def start_page(): def install_sql_hook(): prev_exec_time = get_current_time() - @event.listens_for(db.engine, "before_execute") - def receive_before_execute(conn, clauseelement, multiparams, params): - nonlocal prev_exec_time - curr = get_current_time() - print( - f"--------------------------------------TIMING: {curr} ({curr - prev_exec_time})" - ) - prev_exec_time = curr - for r in traceback.format_stack(): - if ( - r.startswith(' File "/service/') - and not receive_before_execute.__name__ in r - ): - print(r, end="") - try: - print(clauseelement) - except Exception as e: - print(f": {e}") + with app.app_context(): + @event.listens_for(db.engine, "before_execute") + def receive_before_execute(conn, clauseelement, multiparams, params): + nonlocal prev_exec_time + curr = get_current_time() + print( + f"--------------------------------------TIMING: {curr} ({curr - prev_exec_time})" + ) + prev_exec_time = curr + for r in traceback.format_stack(): + if ( + r.startswith(' File "/service/') + and not receive_before_execute.__name__ in r + ): + print(r, end="") + try: + print(clauseelement, multiparams, params) + except Exception as e: + print(f": {e}") if app.config["TESTING"]: diff --git a/timApp/timdb/sqa.py b/timApp/timdb/sqa.py index ef3c70d9e5..3c10c5654a 100644 --- a/timApp/timdb/sqa.py +++ b/timApp/timdb/sqa.py @@ -10,6 +10,7 @@ from flask_sqlalchemy import SQLAlchemy from sqlalchemy import func, text +from sqlalchemy.orm import mapped_column from sqlalchemy.orm.base import instance_state session_options = { @@ -27,8 +28,8 @@ class TimeStampMixin: - created = db.Column(db.DateTime(timezone=True), nullable=True, default=func.now()) - modified = db.Column( + created = mapped_column(db.DateTime(timezone=True), nullable=True, default=func.now()) + modified = mapped_column( db.DateTime(timezone=True), nullable=True, default=func.now(), diff --git a/timApp/user/consentchange.py b/timApp/user/consentchange.py index b6f6faa77a..6408ddbffb 100644 --- a/timApp/user/consentchange.py +++ b/timApp/user/consentchange.py @@ -1,4 +1,5 @@ from sqlalchemy import func +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db from timApp.user.user import Consent @@ -6,11 +7,11 @@ class ConsentChange(db.Model): __tablename__ = "consentchange" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) - time = db.Column(db.DateTime(timezone=True), nullable=False, default=func.now()) - consent = db.Column(db.Enum(Consent), nullable=False) + + id = mapped_column(db.Integer, primary_key=True) + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + time = mapped_column(db.DateTime(timezone=True), nullable=False, default=func.now()) + consent = mapped_column(db.Enum(Consent), nullable=False) user = db.relationship("User", back_populates="consents", lazy="select") diff --git a/timApp/user/hakaorganization.py b/timApp/user/hakaorganization.py index 713b3c1f10..053a22d9da 100644 --- a/timApp/user/hakaorganization.py +++ b/timApp/user/hakaorganization.py @@ -2,15 +2,16 @@ from flask import current_app from sqlalchemy import select +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db class HakaOrganization(db.Model): - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.Text, nullable=False, unique=True) + + id = mapped_column(db.Integer, primary_key=True) + name = mapped_column(db.Text, nullable=False, unique=True) uniquecodes = db.relationship("PersonalUniqueCode", back_populates="organization") diff --git a/timApp/user/newuser.py b/timApp/user/newuser.py index 2f06c33f21..76acd736dd 100644 --- a/timApp/user/newuser.py +++ b/timApp/user/newuser.py @@ -1,4 +1,5 @@ from sqlalchemy import func +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db from timApp.user.userutils import check_password_hash @@ -8,15 +9,17 @@ class NewUser(db.Model): """A user that is going to register to TIM via email and has not yet completed the registration process.""" __tablename__ = "newuser" - __allow_unmapped__ = True - email = db.Column(db.Text, primary_key=True) + + email = mapped_column(db.Text, primary_key=True) """Email address.""" - pass_ = db.Column("pass", db.Text, nullable=False, primary_key=True) + pass_ = mapped_column("pass", db.Text, nullable=False, primary_key=True) """Password hash for the temporary password.""" - created = db.Column(db.DateTime(timezone=True), nullable=False, default=func.now()) + created = mapped_column( + db.DateTime(timezone=True), nullable=False, default=func.now() + ) """The time when user clicked "Sign up".""" def check_password(self, password: str) -> bool: diff --git a/timApp/user/personaluniquecode.py b/timApp/user/personaluniquecode.py index 85b4045883..9367324015 100644 --- a/timApp/user/personaluniquecode.py +++ b/timApp/user/personaluniquecode.py @@ -3,6 +3,7 @@ from typing import Optional from sqlalchemy import select +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db from timApp.user.hakaorganization import HakaOrganization @@ -11,14 +12,14 @@ class PersonalUniqueCode(db.Model): """The database model for the 'schacPersonalUniqueCode' Haka attribute.""" - __allow_unmapped__ = True + - user_id = db.Column( + user_id = mapped_column( db.Integer, db.ForeignKey("useraccount.id"), nullable=False, primary_key=True ) """User id.""" - org_id = db.Column( + org_id = mapped_column( db.Integer, db.ForeignKey("haka_organization.id"), nullable=False, @@ -26,10 +27,10 @@ class PersonalUniqueCode(db.Model): ) """Organization id.""" - code = db.Column(db.Text, nullable=False, index=True) + code = mapped_column(db.Text, nullable=False, index=True) """The actual code. This could be e.g. student id or employee id.""" - type = db.Column(db.Text, nullable=False, primary_key=True) + type = mapped_column(db.Text, nullable=False, primary_key=True) """The type of the code, e.g. student or employee.""" user = db.relationship("User", back_populates="uniquecodes", lazy="selectin") diff --git a/timApp/user/user.py b/timApp/user/user.py index cd802ed53d..6b753829a1 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -3,17 +3,19 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Optional, Union, MutableMapping +from typing import Optional, Union import filelock from flask import current_app, has_request_context from sqlalchemy import func, select, delete from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import selectinload, defaultload -from sqlalchemy.orm.collections import ( +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import ( + selectinload, + defaultload, attribute_mapped_collection, ) -from sqlalchemy.orm.strategy_options import loader_option +from sqlalchemy.orm.interfaces import LoaderOption from sqlalchemy.sql import Select from timApp.answer.answer import Answer @@ -30,9 +32,6 @@ from timApp.item.item import ItemBase from timApp.lecture.lectureusers import LectureUsers from timApp.messaging.messagelist.listinfo import Channel -from timApp.messaging.timMessage.internalmessage_models import ( - InternalMessageReadReceipt, -) from timApp.notification.notification import Notification, NotificationType from timApp.sisu.scimusergroup import ScimUserGroup from timApp.timdb.exceptions import TimDbException @@ -248,36 +247,36 @@ class User(db.Model, TimeStampMixin, SCIMEntity): """ __tablename__ = "useraccount" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) + + id = mapped_column(db.Integer, primary_key=True) """User identifier.""" - name = db.Column(db.Text, nullable=False, unique=True) + name = mapped_column(db.Text, nullable=False, unique=True) """User name (not full name). Used to identify the user and during log-in.""" - given_name = db.Column(db.Text) + given_name = mapped_column(db.Text) """User's given name.""" - last_name = db.Column(db.Text) + last_name = mapped_column(db.Text) """User's last name.""" - real_name = db.Column(db.Text) + real_name = mapped_column(db.Text) """Real (full) name. This may be in the form "Lastname Firstname" or "Firstname Lastname".""" - _email = db.Column("email", db.Text, unique=True) + _email = mapped_column("email", db.Text, unique=True) """Email address.""" - prefs = db.Column(db.Text) + prefs = mapped_column(db.Text) """Preferences as a JSON string.""" - pass_ = db.Column("pass", db.Text) + pass_ = mapped_column("pass", db.Text) """Password hashed with bcrypt.""" - consent = db.Column(db.Enum(Consent), nullable=True) + consent = mapped_column(db.Enum(Consent), nullable=True) """Current consent for cookie/data collection.""" - origin = db.Column(db.Enum(UserOrigin), nullable=True) + origin = mapped_column(db.Enum(UserOrigin), nullable=True) """How the user registered to TIM.""" uniquecodes = db.relationship( @@ -287,9 +286,9 @@ class User(db.Model, TimeStampMixin, SCIMEntity): ) """Personal unique codes used to identify the user via Haka Identity Provider.""" - internalmessage_readreceipt: InternalMessageReadReceipt | None = db.relationship( + internalmessage_readreceipt = db.relationship( "InternalMessageReadReceipt", back_populates="user" - ) + ) # : InternalMessageReadReceipt | None """User's read receipts for internal messages.""" primary_email_contact = db.relationship( @@ -325,13 +324,13 @@ def _set_email(self, value: str) -> None: consents = db.relationship("ConsentChange", back_populates="user", lazy="select") """User's consent changes.""" - contacts: list[UserContact] = db.relationship( + contacts = db.relationship( "UserContact", back_populates="user", lazy="select", overlaps="primary_email_contact", cascade_backrefs=False, - ) + ) # : list[UserContact] """User's contacts.""" notifications = db.relationship( @@ -342,14 +341,14 @@ def _set_email(self, value: str) -> None: ) """Notification settings for the user. Represents what notifications the user wants to receive.""" - groups: list[UserGroup] = db.relationship( + groups = db.relationship( UserGroup, UserGroupMember.__table__, primaryjoin=(id == UserGroupMember.user_id) & membership_current, back_populates="users", lazy="select", overlaps="user, current_memberships, group, memberships, memberships_sel", - ) + ) # : list[UserGroup] """Current groups of the user is a member of.""" groups_dyn = db.relationship( @@ -378,11 +377,11 @@ def _set_email(self, value: str) -> None: ) """User's group memberships as a dynamic query.""" - memberships: list[UserGroupMember] = db.relationship( + memberships = db.relationship( UserGroupMember, foreign_keys="UserGroupMember.user_id", overlaps="groups_inactive, memberships_dyn, user, users", - ) + ) # : list[UserGroupMember] """All user's group memberships.""" active_memberships = db.relationship( @@ -438,17 +437,17 @@ def _set_email(self, value: str) -> None: velps = db.relationship("Velp", back_populates="creator", lazy="dynamic") """Velps created by the user as a dynamic query.""" - sessions: list[UserSession] = db.relationship( + sessions = db.relationship( "UserSession", back_populates="user", lazy="select", cascade_backrefs=False - ) + ) # : list[UserSession] """All user's sessions as a dynamic query.""" - active_sessions: MutableMapping[str, UserSession] = db.relationship( + active_sessions = db.relationship( "UserSession", primaryjoin=(id == UserSession.user_id) & ~UserSession.expired, collection_class=attribute_mapped_collection("session_id"), overlaps="sessions, user", - ) + ) # : MutableMapping[str, UserSession] """Active sessions mapped by the session ID.""" # Used for copying @@ -717,9 +716,13 @@ def get_by_id(uid: int) -> Optional["User"]: def get_by_email(email: str) -> Optional["User"]: if email is None: raise Exception("Tried to find an user by null email") - return db.session.execute( - user_query_with_joined_groups().filter_by(email=email).limit(1) - ).scalars().first() + return ( + db.session.execute( + user_query_with_joined_groups().filter_by(email=email).limit(1) + ) + .scalars() + .first() + ) @staticmethod def get_by_email_case_insensitive(email: str) -> list["User"]: @@ -929,7 +932,7 @@ def get_contact( self, channel: Channel, contact: str, - options: list[loader_option] | None = None, + options: list[LoaderOption] | None = None, ) -> UserContact | None: """Find user's contact by channel and contact contents. diff --git a/timApp/user/usercontact.py b/timApp/user/usercontact.py index 00a14688e3..5a3fcf80b7 100644 --- a/timApp/user/usercontact.py +++ b/timApp/user/usercontact.py @@ -1,5 +1,7 @@ from enum import Enum +from sqlalchemy.orm import mapped_column + from timApp.messaging.messagelist.listinfo import Channel from timApp.timdb.sqa import db @@ -32,7 +34,7 @@ class UserContact(db.Model): """TIM users' additional contact information.""" __tablename__ = "usercontact" - __allow_unmapped__ = True + __table_args__ = ( # A user should not have the same contact for the channel @@ -55,26 +57,28 @@ class UserContact(db.Model): ), ) - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) """Which user owns this contact information.""" - contact = db.Column(db.Text, nullable=False) + contact = mapped_column(db.Text, nullable=False) """Contact identifier for a channel.""" - channel = db.Column(db.Enum(Channel), nullable=False) + channel = mapped_column(db.Enum(Channel), nullable=False) """Channel the contact information points to.""" - verified = db.Column(db.Boolean, nullable=False, default=False) + verified = mapped_column(db.Boolean, nullable=False, default=False) """Whether this contact info is verified by the user. If False, the user has made a claim for a contact info, but has not yet verified it's ownership.""" - primary = db.Column(db.Enum(PrimaryContact)) + primary = mapped_column(db.Enum(PrimaryContact)) """Whether the contact is primary for the user""" - contact_origin: ContactOrigin = db.Column(db.Enum(ContactOrigin), nullable=False) + contact_origin = mapped_column( + db.Enum(ContactOrigin), nullable=False + ) # : ContactOrigin """How the contact was added.""" user = db.relationship("User", back_populates="contacts", lazy="select") diff --git a/timApp/user/usergroup.py b/timApp/user/usergroup.py index e573fa2efb..7b9643e79a 100644 --- a/timApp/user/usergroup.py +++ b/timApp/user/usergroup.py @@ -5,15 +5,9 @@ import attr from sqlalchemy import select -from sqlalchemy.orm import selectinload -from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.orm import selectinload, mapped_column, attribute_mapped_collection from sqlalchemy.sql import Select -from timApp.auth.auth_models import BlockAccess -from timApp.messaging.messagelist.messagelist_models import MessageListTimMember -from timApp.messaging.timMessage.internalmessage_models import ( - InternalMessageDisplay, -) from timApp.sisu.parse_display_name import parse_sisu_group_display_name from timApp.sisu.scimusergroup import ScimUserGroup from timApp.timdb.sqa import db, TimeStampMixin, include_if_exists, is_attribute_loaded @@ -61,15 +55,15 @@ class UserGroup(db.Model, TimeStampMixin, SCIMEntity): """ __tablename__ = "usergroup" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) + + id = mapped_column(db.Integer, primary_key=True) """Usergroup identifier.""" - name = db.Column(db.Text, nullable=False, unique=True) + name = mapped_column(db.Text, nullable=False, unique=True) """Usergroup name (textual identifier).""" - display_name = db.Column(db.Text, nullable=True) + display_name = mapped_column(db.Text, nullable=True) """Usergroup display name. Currently only used for storing certain Sisu course properties: - course code - period (P1...P5) @@ -101,26 +95,26 @@ def scim_display_name(self): cascade="all, delete-orphan", overlaps="memberships, users", ) - current_memberships: dict[int, UserGroupMember] = db.relationship( + current_memberships = db.relationship( UserGroupMember, primaryjoin=(id == UserGroupMember.usergroup_id) & membership_current, collection_class=attribute_mapped_collection("user_id"), back_populates="group", overlaps="memberships, memberships_sel, users", - ) + ) # : dict[int, UserGroupMember] accesses = db.relationship( "BlockAccess", back_populates="usergroup", lazy="dynamic", cascade_backrefs=False, ) - accesses_alt: dict[tuple[int, int], BlockAccess] = db.relationship( + accesses_alt = db.relationship( "BlockAccess", collection_class=attribute_mapped_collection("group_collection_key"), cascade="all, delete-orphan", overlaps="accesses, usergroup", cascade_backrefs=False, - ) + ) # : dict[tuple[int, int], BlockAccess] readparagraphs = db.relationship( "ReadParagraph", back_populates="usergroup", lazy="dynamic" ) @@ -128,28 +122,30 @@ def scim_display_name(self): "ReadParagraph", overlaps="readparagraphs, usergroup", ) - notes = db.relationship("UserNote", back_populates="usergroup", lazy="dynamic", cascade_backrefs=False) + notes = db.relationship( + "UserNote", back_populates="usergroup", lazy="dynamic", cascade_backrefs=False + ) notes_alt = db.relationship("UserNote", overlaps="notes, usergroup") - admin_doc: Block = db.relationship( + admin_doc = db.relationship( "Block", secondary=UserGroupDoc.__table__, lazy="select", uselist=False, - ) + ) # : Block # For groups created from SCIM API - external_id: ScimUserGroup = db.relationship( + external_id = db.relationship( "ScimUserGroup", lazy="select", uselist=False - ) + ) # : ScimUserGroup - messagelist_membership: list[MessageListTimMember] = db.relationship( + messagelist_membership = db.relationship( "MessageListTimMember", back_populates="user_group" - ) + ) # : list[MessageListTimMember] - internalmessage_display: InternalMessageDisplay | None = db.relationship( + internalmessage_display = db.relationship( "InternalMessageDisplay", back_populates="usergroup", cascade_backrefs=False - ) + ) # : InternalMessageDisplay | None def __repr__(self): return f"" diff --git a/timApp/user/usergroupdoc.py b/timApp/user/usergroupdoc.py index e9d16de73c..9dda0389e6 100644 --- a/timApp/user/usergroupdoc.py +++ b/timApp/user/usergroupdoc.py @@ -1,3 +1,5 @@ +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db @@ -7,7 +9,7 @@ class UserGroupDoc(db.Model): """ __tablename__ = "usergroupdoc" - __allow_unmapped__ = True + - group_id = db.Column(db.Integer, db.ForeignKey("usergroup.id"), primary_key=True) - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + group_id = mapped_column(db.Integer, db.ForeignKey("usergroup.id"), primary_key=True) + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) diff --git a/timApp/user/usergroupmember.py b/timApp/user/usergroupmember.py index ff38b4214b..7826e3e17a 100644 --- a/timApp/user/usergroupmember.py +++ b/timApp/user/usergroupmember.py @@ -13,6 +13,7 @@ from datetime import timedelta from sqlalchemy import func +from sqlalchemy.orm import mapped_column from timApp.timdb.sqa import db from timApp.util.utils import get_current_time @@ -24,31 +25,35 @@ class UserGroupMember(db.Model): """ __tablename__ = "usergroupmember" - __allow_unmapped__ = True + - usergroup_id = db.Column( + usergroup_id = mapped_column( db.Integer, db.ForeignKey("usergroup.id"), primary_key=True ) """ID of the usergroup the member belongs to.""" - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) + user_id = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + ) """ID of the user that belongs to the usergroup.""" - membership_end = db.Column(db.DateTime(timezone=True)) + membership_end = mapped_column(db.DateTime(timezone=True)) """Timestamp for when the membership ended. .. note:: The timestamp is used to determine soft deletion. If the end timestamp is present, the user is considered deleted from the group. """ - membership_added = db.Column(db.DateTime(timezone=True), default=get_current_time) + membership_added = mapped_column( + db.DateTime(timezone=True), default=get_current_time + ) """Timestamp for when the user was last time added as the active member. .. note:: The timestamp is used **for logging purposes only**. In other words, it is not used to determine soft deletion or other membership state. """ - added_by = db.Column(db.Integer, db.ForeignKey("useraccount.id")) + added_by = mapped_column(db.Integer, db.ForeignKey("useraccount.id")) """User ID of the user who added the membership.""" user = db.relationship( diff --git a/timApp/user/verification/verification.py b/timApp/user/verification/verification.py index ee4eedf58a..5b298ef8ed 100644 --- a/timApp/user/verification/verification.py +++ b/timApp/user/verification/verification.py @@ -5,7 +5,7 @@ from flask import render_template_string, url_for from sqlalchemy import select -from sqlalchemy.orm import load_only +from sqlalchemy.orm import load_only, mapped_column from timApp.document.docentry import DocEntry from timApp.timdb.sqa import db @@ -50,24 +50,24 @@ class Verification(db.Model): verification.""" __tablename__ = "verification" - __allow_unmapped__ = True + - token = db.Column(db.Text, primary_key=True) + token = mapped_column(db.Text, primary_key=True) """Verification token used for action verification""" - type: VerificationType = db.Column(db.Enum(VerificationType), primary_key=True) + type = mapped_column(db.Enum(VerificationType), primary_key=True) # : VerificationType """The type of verification, see VerificationType class for details.""" - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) """User that can react to verification request.""" - requested_at = db.Column(db.DateTime(timezone=True)) + requested_at = mapped_column(db.DateTime(timezone=True)) """When a verification has been added to db, pending sending to a user.""" - reacted_at = db.Column(db.DateTime(timezone=True)) + reacted_at = mapped_column(db.DateTime(timezone=True)) """When the user reacted to verification request.""" - user: User = db.relationship("User", lazy="select") + user = db.relationship("User", lazy="select") # : User """User that can react to verification request.""" @property @@ -87,11 +87,11 @@ def to_json(self) -> dict: class ContactAddVerification(Verification): - contact_id = db.Column(db.Integer, db.ForeignKey("usercontact.id")) + contact_id = mapped_column(db.Integer, db.ForeignKey("usercontact.id")) - contact: UserContact | None = db.relationship( + contact = db.relationship( "UserContact", lazy="select", uselist=False - ) + ) # : UserContact | None """Contact to verify.""" @property diff --git a/timApp/velp/annotation_model.py b/timApp/velp/annotation_model.py index 6588d0188e..dc145b3739 100644 --- a/timApp/velp/annotation_model.py +++ b/timApp/velp/annotation_model.py @@ -2,6 +2,8 @@ from dataclasses import dataclass from datetime import datetime +from sqlalchemy.orm import mapped_column + from timApp.timdb.sqa import db @@ -44,36 +46,36 @@ class Annotation(db.Model): """ __tablename__ = "annotation" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) + + id = mapped_column(db.Integer, primary_key=True) """Annotation identifier.""" - velp_version_id = db.Column( + velp_version_id = mapped_column( db.Integer, db.ForeignKey("velpversion.id"), nullable=False ) """Id of the velp that has been used for this annotation.""" - annotator_id = db.Column( + annotator_id = mapped_column( db.Integer, db.ForeignKey("useraccount.id"), nullable=False ) """Id of the User who created the annotation.""" - points = db.Column(db.Float) + points = mapped_column(db.Float) """Points associated with the annotation.""" - creation_time = db.Column( + creation_time = mapped_column( db.DateTime(timezone=True), nullable=False, default=datetime.utcnow ) """Creation time.""" - valid_from = db.Column(db.DateTime(timezone=True), default=datetime.utcnow) + valid_from = mapped_column(db.DateTime(timezone=True), default=datetime.utcnow) """Since when should this annotation be valid.""" - valid_until = db.Column(db.DateTime(timezone=True)) + valid_until = mapped_column(db.DateTime(timezone=True)) """Until when should this annotation be valid.""" - visible_to = db.Column(db.Integer) + visible_to = mapped_column(db.Integer) """Who should this annotation be visible to. Possible values are denoted by AnnotationVisibility enum: @@ -85,55 +87,55 @@ class Annotation(db.Model): """ - document_id = db.Column(db.Integer, db.ForeignKey("block.id")) + document_id = mapped_column(db.Integer, db.ForeignKey("block.id")) """Id of the document in case this is a paragraph annotation.""" - answer_id = db.Column(db.Integer, db.ForeignKey("answer.id")) + answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id")) """Id of the Answer in case this is an answer annotation.""" - paragraph_id_start = db.Column(db.Text) + paragraph_id_start = mapped_column(db.Text) """The id of the paragraph where this annotation starts from (in case this is a paragraph annotation).""" - paragraph_id_end = db.Column(db.Text) + paragraph_id_end = mapped_column(db.Text) """The id of the paragraph where this annotation ends (in case this is a paragraph annotation).""" - offset_start = db.Column(db.Integer) + offset_start = mapped_column(db.Integer) """Positional information about the annotation.""" - node_start = db.Column(db.Integer) + node_start = mapped_column(db.Integer) """Positional information about the annotation.""" - depth_start = db.Column(db.Integer) + depth_start = mapped_column(db.Integer) """Positional information about the annotation.""" - offset_end = db.Column(db.Integer) + offset_end = mapped_column(db.Integer) """Positional information about the annotation.""" - node_end = db.Column(db.Integer) + node_end = mapped_column(db.Integer) """Positional information about the annotation.""" - depth_end = db.Column(db.Integer) + depth_end = mapped_column(db.Integer) """Positional information about the annotation.""" - hash_start = db.Column(db.Text) + hash_start = mapped_column(db.Text) """Positional information about the annotation.""" - hash_end = db.Column(db.Text) + hash_end = mapped_column(db.Text) """Positional information about the annotation.""" - color = db.Column(db.Text) + color = mapped_column(db.Text) """Color for the annotation.""" - element_path_start = db.Column(db.Text) + element_path_start = mapped_column(db.Text) """Positional information about the annotation.""" - element_path_end = db.Column(db.Text) + element_path_end = mapped_column(db.Text) """Positional information about the annotation.""" - draw_data = db.Column(db.Text) + draw_data = mapped_column(db.Text) """Drawing information about the annotation (for annotations on images).""" - style = db.Column(db.Integer) + style = mapped_column(db.Integer) """Appearance of the annotation""" annotator = db.relationship("User", back_populates="annotations") diff --git a/timApp/velp/annotations.py b/timApp/velp/annotations.py index 47b9098238..3244b056ba 100644 --- a/timApp/velp/annotations.py +++ b/timApp/velp/annotations.py @@ -63,54 +63,47 @@ def get_annotations_with_comments_in_document( answer_filter = (User.id == user.id) | (User.id == None) if is_peerreview_enabled(d): answer_filter |= User.id.in_( - get_reviews_where_user_is_reviewer_query(d, user) - .with_only_columns(PeerReview.reviewable_id) + get_reviews_where_user_is_reviewer_query(d, user).with_only_columns( + PeerReview.reviewable_id + ) ) own_review_filter = (User.id == user.id) | ( Annotation.annotator_id == user.id ) - q = (set_annotation_query_opts( - select(Annotation) - .filter_by(document_id=d.id) - .filter( - (Annotation.valid_until == None) - | (Annotation.valid_until >= func.current_timestamp()) - ) - .filter((Annotation.annotator_id == user.id) | vis_filter) - .join(VelpVersion) - .join(Velp) - .join(VelpContent) - .filter(VelpContent.language_id == language_id) - .outerjoin(Answer) - .outerjoin(User, Answer.users_all) - .filter(answer_filter) - .filter(own_review_filter) - .order_by( - Annotation.depth_start.desc(), - Annotation.node_start.desc(), - Annotation.offset_start.desc(), - ) + q = ( + set_annotation_query_opts( + select(Annotation) + .filter_by(document_id=d.id) + .filter( + (Annotation.valid_until == None) + | (Annotation.valid_until >= func.current_timestamp()) ) - .options(joinedload(Annotation.velp_content)) - .options(joinedload(Annotation.answer).selectinload(Answer.users_all)) - .options( - joinedload(Annotation.velp_version).joinedload(VelpVersion.velp) + .filter((Annotation.annotator_id == user.id) | vis_filter) + .join(VelpVersion) + .join(Velp) + .join(VelpContent) + .filter(VelpContent.language_id == language_id) + .outerjoin(Answer) + .outerjoin(User, Answer.users_all) + .filter(answer_filter) + .filter(own_review_filter) + .order_by( + Annotation.depth_start.desc(), + Annotation.node_start.desc(), + Annotation.offset_start.desc(), ) - .with_only_columns(Annotation)) - anns = ( - db.session.execute( - q ) - .scalars() - .all() + .options(joinedload(Annotation.answer).selectinload(Answer.users_all)) + .with_only_columns(Annotation) ) + anns = db.session.execute(q).scalars().all() return anns def set_annotation_query_opts(q: Select) -> Select: return ( - q.options(selectinload(Annotation.velp_content).load_only(VelpContent.content)) + q.options(joinedload(Annotation.velp_content).load_only(VelpContent.content)) .options( selectinload(Annotation.comments) .joinedload(AnnotationComment.commenter) @@ -123,7 +116,7 @@ def set_annotation_query_opts(q: Select) -> Select: .raiseload(User.groups) ) .options( - selectinload(Annotation.velp_version) + joinedload(Annotation.velp_version) .load_only(VelpVersion.id, VelpVersion.velp_id) .joinedload(VelpVersion.velp) .load_only(Velp.color) diff --git a/timApp/velp/velp_models.py b/timApp/velp/velp_models.py index 3f0a5e0813..07733caf53 100644 --- a/timApp/velp/velp_models.py +++ b/timApp/velp/velp_models.py @@ -1,9 +1,9 @@ """Defines all data models related to velps.""" from datetime import datetime +from sqlalchemy.orm import mapped_column from sqlalchemy.orm.collections import attribute_mapped_collection # type: ignore -from timApp.item.block import Block from timApp.timdb.sqa import db @@ -11,14 +11,14 @@ class VelpContent(db.Model): """The actual content of a Velp.""" __tablename__ = "velpcontent" - __allow_unmapped__ = True + - version_id = db.Column( + version_id = mapped_column( db.Integer, db.ForeignKey("velpversion.id"), primary_key=True ) - language_id = db.Column(db.Text, primary_key=True) - content = db.Column(db.Text) - default_comment = db.Column(db.Text) + language_id = mapped_column(db.Text, primary_key=True) + content = mapped_column(db.Text) + default_comment = mapped_column(db.Text) velp_version = db.relationship("VelpVersion") @@ -27,27 +27,27 @@ class AnnotationComment(db.Model): """A comment in an Annotation.""" __tablename__ = "annotationcomment" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) """Comment identifier.""" - annotation_id = db.Column( + annotation_id = mapped_column( db.Integer, db.ForeignKey("annotation.id"), nullable=False ) """Annotation id.""" - comment_time = db.Column( + comment_time = mapped_column( db.DateTime(timezone=True), nullable=False, default=datetime.utcnow ) """Comment timestamp.""" - commenter_id = db.Column( + commenter_id = mapped_column( db.Integer, db.ForeignKey("useraccount.id"), nullable=False ) """Commenter user id.""" - content = db.Column(db.Text) + content = mapped_column(db.Text) """Comment text.""" commenter = db.relationship("User") @@ -66,39 +66,43 @@ class LabelInVelp(db.Model): """Associates VelpLabels with Velps.""" __tablename__ = "labelinvelp" - __allow_unmapped__ = True + - label_id = db.Column(db.Integer, db.ForeignKey("velplabel.id"), primary_key=True) - velp_id = db.Column(db.Integer, db.ForeignKey("velp.id"), primary_key=True) + label_id = mapped_column( + db.Integer, db.ForeignKey("velplabel.id"), primary_key=True + ) + velp_id = mapped_column(db.Integer, db.ForeignKey("velp.id"), primary_key=True) class VelpInGroup(db.Model): __tablename__ = "velpingroup" - __allow_unmapped__ = True + - velp_group_id = db.Column( + velp_group_id = mapped_column( db.Integer, db.ForeignKey("velpgroup.id"), primary_key=True ) - velp_id = db.Column(db.Integer, db.ForeignKey("velp.id"), primary_key=True) + velp_id = mapped_column(db.Integer, db.ForeignKey("velp.id"), primary_key=True) class Velp(db.Model): """A Velp is a kind of category for Annotations and is visually represented by a Post-it note.""" __tablename__ = "velp" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) - creator_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) - creation_time = db.Column( + id = mapped_column(db.Integer, primary_key=True) + creator_id = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), nullable=False + ) + creation_time = mapped_column( db.DateTime(timezone=True), nullable=False, default=datetime.utcnow ) - default_points = db.Column(db.Float) - valid_from = db.Column(db.DateTime(timezone=True), default=datetime.utcnow) - valid_until = db.Column(db.DateTime(timezone=True)) - color = db.Column(db.Text) - visible_to = db.Column(db.Integer, nullable=False) - style = db.Column(db.Integer) + default_points = mapped_column(db.Float) + valid_from = mapped_column(db.DateTime(timezone=True), default=datetime.utcnow) + valid_until = mapped_column(db.DateTime(timezone=True)) + color = mapped_column(db.Text) + visible_to = mapped_column(db.Integer, nullable=False) + style = mapped_column(db.Integer) creator = db.relationship("User", back_populates="velps") labels = db.relationship( @@ -114,9 +118,9 @@ class Velp(db.Model): collection_class=attribute_mapped_collection("id"), cascade="all", ) - velp_versions: list["VelpVersion"] = db.relationship( + velp_versions = db.relationship( "VelpVersion", order_by="VelpVersion.id.desc()" - ) + ) # : list["VelpVersion"] def to_json(self) -> dict: vv = self.velp_versions[0] @@ -140,16 +144,16 @@ class VelpGroup(db.Model): """Represents a group of Velps.""" __tablename__ = "velpgroup" - __allow_unmapped__ = True + - id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - name = db.Column(db.Text) - creation_time = db.Column( + id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + name = mapped_column(db.Text) + creation_time = mapped_column( db.DateTime(timezone=True), nullable=False, default=datetime.utcnow ) - valid_from = db.Column(db.DateTime(timezone=True), default=datetime.utcnow) - valid_until = db.Column(db.DateTime(timezone=True)) - default_group = db.Column(db.Boolean, default=False) + valid_from = mapped_column(db.DateTime(timezone=True), default=datetime.utcnow) + valid_until = mapped_column(db.DateTime(timezone=True)) + default_group = mapped_column(db.Boolean, default=False) velps = db.relationship( "Velp", @@ -158,10 +162,10 @@ class VelpGroup(db.Model): collection_class=attribute_mapped_collection("id"), cascade="all", ) - block: Block = db.relationship( + block = db.relationship( "Block", lazy="selectin", - ) + ) # : Block # docentry = db.relationship( # 'DocEntry', # ) @@ -176,41 +180,43 @@ def to_json(self) -> dict: class VelpGroupDefaults(db.Model): __tablename__ = "velpgroupdefaults" - __allow_unmapped__ = True + - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - target_type = db.Column( + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + target_type = mapped_column( db.Integer, nullable=False ) # 0 = document, 1 = paragraph, 2 = area - target_id = db.Column(db.Text, primary_key=True) - velp_group_id = db.Column( + target_id = mapped_column(db.Text, primary_key=True) + velp_group_id = mapped_column( db.Integer, db.ForeignKey("velpgroup.id"), primary_key=True ) - selected = db.Column(db.Boolean, default=False) + selected = mapped_column(db.Boolean, default=False) class VelpGroupLabel(db.Model): """Currently not used (0 rows in production DB as of 5th July 2018).""" __tablename__ = "velpgrouplabel" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) - content = db.Column(db.Text, nullable=False) + id = mapped_column(db.Integer, primary_key=True) + content = mapped_column(db.Text, nullable=False) class VelpGroupSelection(db.Model): __tablename__ = "velpgroupselection" - __allow_unmapped__ = True + - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - target_type = db.Column( + user_id = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + ) + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + target_type = mapped_column( db.Integer, nullable=False ) # 0 = document, 1 = paragraph, 2 = area - target_id = db.Column(db.Text, primary_key=True) - selected = db.Column(db.Boolean, default=False) - velp_group_id = db.Column( + target_id = mapped_column(db.Text, primary_key=True) + selected = mapped_column(db.Boolean, default=False) + velp_group_id = mapped_column( db.Integer, db.ForeignKey("velpgroup.id"), primary_key=True ) @@ -223,11 +229,13 @@ class VelpGroupsInDocument(db.Model): """ __tablename__ = "velpgroupsindocument" - __allow_unmapped__ = True + - user_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) - doc_id = db.Column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - velp_group_id = db.Column( + user_id = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + ) + doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + velp_group_id = mapped_column( db.Integer, db.ForeignKey("velpgroup.id"), primary_key=True ) @@ -236,11 +244,13 @@ class VelpLabel(db.Model): """A label that can be assigned to a Velp.""" __tablename__ = "velplabel" - __allow_unmapped__ = True + - id = db.Column(db.Integer, primary_key=True) + id = mapped_column(db.Integer, primary_key=True) # TODO make not nullable - creator_id = db.Column(db.Integer, db.ForeignKey("useraccount.id"), nullable=True) + creator_id = mapped_column( + db.Integer, db.ForeignKey("useraccount.id"), nullable=True + ) creator = db.relationship("User") velps = db.relationship( @@ -253,13 +263,13 @@ class VelpLabel(db.Model): class VelpLabelContent(db.Model): __tablename__ = "velplabelcontent" - __allow_unmapped__ = True + - velplabel_id = db.Column( + velplabel_id = mapped_column( db.Integer, db.ForeignKey("velplabel.id"), primary_key=True ) - language_id = db.Column(db.Text, primary_key=True) - content = db.Column(db.Text) + language_id = mapped_column(db.Text, primary_key=True) + content = mapped_column(db.Text) velplabel = db.relationship("VelpLabel") @@ -273,13 +283,13 @@ def to_json(self) -> dict: class VelpVersion(db.Model): __tablename__ = "velpversion" - __allow_unmapped__ = True - id = db.Column(db.Integer, primary_key=True) - velp_id = db.Column(db.Integer, db.ForeignKey("velp.id"), nullable=False) - modify_time = db.Column( + + id = mapped_column(db.Integer, primary_key=True) + velp_id = mapped_column(db.Integer, db.ForeignKey("velp.id"), nullable=False) + modify_time = mapped_column( db.DateTime(timezone=True), nullable=False, default=datetime.utcnow ) - velp: Velp = db.relationship("Velp", overlaps="velp_versions") - content: list[VelpContent] = db.relationship("VelpContent", overlaps="velp_version") + velp = db.relationship("Velp", overlaps="velp_versions") # : Velp + content = db.relationship("VelpContent", overlaps="velp_version") # : list[VelpContent] From a6c93689fdd60f27301eb001368df11d3a3e57ee Mon Sep 17 00:00:00 2001 From: dezhidki Date: Thu, 27 Jul 2023 18:07:10 +0300 Subject: [PATCH 08/34] Convert DB models to strongly typed Mapped models --- poetry.lock | 16 +- timApp/answer/answer.py | 65 +++--- timApp/answer/answer_models.py | 34 +-- timApp/auth/auth_models.py | 45 ++-- timApp/auth/oauth2/models.py | 92 ++++++-- timApp/auth/session/model.py | 21 +- timApp/celery_sqlalchemy_scheduler/models.py | 105 ++++----- timApp/document/docentry.py | 20 +- timApp/document/translation/language.py | 14 +- timApp/document/translation/translation.py | 19 +- timApp/document/translation/translator.py | 22 +- timApp/folder/folder.py | 11 +- timApp/item/block.py | 91 ++++---- timApp/item/blockassociation.py | 7 +- timApp/item/blockrelevance.py | 13 +- timApp/item/tag.py | 18 +- timApp/item/taskblock.py | 9 +- timApp/lecture/askedjson.py | 11 +- timApp/lecture/askedquestion.py | 64 +++--- timApp/lecture/lecture.py | 49 ++-- timApp/lecture/lectureanswer.py | 33 ++- timApp/lecture/lectureusers.py | 11 +- timApp/lecture/message.py | 26 +-- timApp/lecture/question.py | 21 +- timApp/lecture/questionactivity.py | 22 +- timApp/lecture/runningquestion.py | 34 +-- timApp/lecture/showpoints.py | 12 +- timApp/lecture/useractivity.py | 23 +- .../messagelist/messagelist_models.py | 153 +++++++------ .../timMessage/internalmessage_models.py | 67 +++--- timApp/note/usernote.py | 40 ++-- timApp/notification/notification.py | 22 +- timApp/notification/pending_notification.py | 36 +-- timApp/peerreview/peerreview.py | 44 ++-- timApp/plugin/calendar/models.py | 136 ++++++----- timApp/plugin/plugintype.py | 9 +- timApp/plugin/timtable/row_owner_info.py | 19 +- timApp/printing/printeddoc.py | 26 +-- timApp/proxy/routes.py | 1 + timApp/readmark/readparagraph.py | 29 +-- timApp/sisu/scimusergroup.py | 11 +- timApp/slide/slidestatus.py | 7 +- timApp/tim.py | 2 +- timApp/timdb/sqa.py | 24 +- timApp/timdb/timdb.py | 134 ----------- timApp/timdb/types.py | 16 ++ timApp/user/consentchange.py | 16 +- timApp/user/hakaorganization.py | 16 +- timApp/user/newuser.py | 12 +- timApp/user/personaluniquecode.py | 38 ++-- timApp/user/user.py | 190 ++++++++-------- timApp/user/usercontact.py | 36 +-- timApp/user/usergroup.py | 100 ++++---- timApp/user/usergroupdoc.py | 9 +- timApp/user/usergroupmember.py | 39 ++-- timApp/user/verification/verification.py | 18 +- timApp/velp/annotation_model.py | 81 +++---- timApp/velp/velp_models.py | 215 ++++++++---------- 58 files changed, 1190 insertions(+), 1264 deletions(-) delete mode 100644 timApp/timdb/timdb.py create mode 100644 timApp/timdb/types.py diff --git a/poetry.lock b/poetry.lock index 0455c6efe5..cb6b8ec794 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3182,20 +3182,6 @@ test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3 timezone = ["python-dateutil"] url = ["furl (>=0.4.1)"] -[[package]] -name = "sqlalchemy2-stubs" -version = "0.0.2a35" -description = "Typing Stubs for SQLAlchemy 1.4" -optional = false -python-versions = ">=3.6" -files = [ - {file = "sqlalchemy2-stubs-0.0.2a35.tar.gz", hash = "sha256:bd5d530697d7e8c8504c7fe792ef334538392a5fb7aa7e4f670bfacdd668a19d"}, - {file = "sqlalchemy2_stubs-0.0.2a35-py3-none-any.whl", hash = "sha256:593784ff9fc0dc2ded1895e3322591689db3be06f3ca006e3ef47640baf2d38a"}, -] - -[package.dependencies] -typing-extensions = ">=3.7.4" - [[package]] name = "trio" version = "0.22.2" @@ -3764,4 +3750,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "d63b45f79f6a7f489f0a9e607725117856d97434a9f8fc22f53c764297da00db" +content-hash = "06cbdd0ef0eeb7af47bd78f37fa371eb599eb8f3cee07ddd073da50c883256b8" diff --git a/timApp/answer/answer.py b/timApp/answer/answer.py index 943d72d89f..2a4278e217 100644 --- a/timApp/answer/answer.py +++ b/timApp/answer/answer.py @@ -1,12 +1,18 @@ import json -from typing import Any +from typing import Any, Optional, List, TYPE_CHECKING from sqlalchemy import func -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped -from timApp.answer.answer_models import UserAnswer +from timApp.answer.answer_models import UserAnswer, AnswerUpload from timApp.plugin.taskid import TaskId from timApp.timdb.sqa import db, include_if_loaded +from timApp.timdb.types import datetime_tz + +if TYPE_CHECKING: + from timApp.user.user import User + from timApp.velp.annotation_model import Annotation + from timApp.plugin.plugintype import PluginType class AnswerSaver(db.Model): @@ -15,11 +21,10 @@ class AnswerSaver(db.Model): """ __tablename__ = "answersaver" - - answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id"), primary_key=True) - user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + answer_id: Mapped[int] = mapped_column(db.ForeignKey("answer.id"), primary_key=True) + user_id: Mapped[int] = mapped_column( + db.ForeignKey("useraccount.id"), primary_key=True ) @@ -27,55 +32,55 @@ class Answer(db.Model): """An answer to a task.""" __tablename__ = "answer" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """Answer identifier.""" - task_id = mapped_column(db.Text, nullable=False, index=True) + task_id: Mapped[str] = mapped_column(index=True) """Task id to which this answer was posted. In the form "doc_id.name", for example "2.task1".""" - origin_doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=True) + origin_doc_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) """The document in which the answer was saved""" - plugin_type_id = mapped_column( - db.Integer, db.ForeignKey("plugintype.id"), nullable=True + plugin_type_id: Mapped[Optional[int]] = mapped_column( + db.ForeignKey("plugintype.id") ) """Plugin type the answer was saved on""" - content = mapped_column(db.Text, nullable=False) + content: Mapped[str] """Answer content.""" - points = mapped_column(db.Float) + points: Mapped[Optional[float]] """Points.""" - answered_on = mapped_column( - db.DateTime(timezone=True), nullable=False, default=func.now() - ) + answered_on: Mapped[datetime_tz] = mapped_column(default=func.now()) """Answer timestamp.""" - valid = mapped_column(db.Boolean, nullable=False) + valid: Mapped[bool] """Whether this answer is valid.""" - last_points_modifier = mapped_column(db.Integer, db.ForeignKey("usergroup.id")) + last_points_modifier: Mapped[Optional[int]] = mapped_column( + db.ForeignKey("usergroup.id") + ) """The UserGroup who modified the points last. Null if the points have been given by the task automatically.""" - plugin_type = db.relationship("PluginType", lazy="select") # : PluginType | None - uploads = db.relationship("AnswerUpload", back_populates="answer", lazy="dynamic") - users = db.relationship( - "User", secondary=UserAnswer.__table__, back_populates="answers", lazy="dynamic" + plugin_type: Mapped["PluginType"] = db.relationship(lazy="select") + uploads: Mapped[List["AnswerUpload"]] = db.relationship( + back_populates="answer", lazy="dynamic" + ) + users: Mapped[List["User"]] = db.relationship( + secondary=UserAnswer.__table__, back_populates="answers", lazy="dynamic" ) - users_all = db.relationship( - "User", + users_all: Mapped[List["User"]] = db.relationship( secondary=UserAnswer.__table__, back_populates="answers_alt", order_by="User.real_name", lazy="select", overlaps="users", ) - annotations = db.relationship("Annotation", back_populates="answer") - saver = db.relationship( - "User", lazy="select", secondary=AnswerSaver.__table__, uselist=False + annotations: Mapped[List["Annotation"]] = db.relationship(back_populates="answer") + saver: Mapped["User"] = db.relationship( + lazy="select", secondary=AnswerSaver.__table__ ) @property @@ -83,7 +88,7 @@ def content_as_json(self) -> dict: return json.loads(self.content) def get_answer_number(self) -> int: - u = self.users.first() + u: User = self.users.first() if not u: return 1 return u.get_answers_for_task(self.task_id).filter(Answer.id <= self.id).count() diff --git a/timApp/answer/answer_models.py b/timApp/answer/answer_models.py index eef194fcc6..1e518742ab 100644 --- a/timApp/answer/answer_models.py +++ b/timApp/answer/answer_models.py @@ -1,7 +1,13 @@ -from sqlalchemy.orm import mapped_column +from typing import TYPE_CHECKING, Optional + +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +if TYPE_CHECKING: + from timApp.item.block import Block + from timApp.answer.answer import Answer + class AnswerTag(db.Model): """Tags for an Answer. @@ -10,26 +16,24 @@ class AnswerTag(db.Model): """ __tablename__ = "answertag" - - id = mapped_column(db.Integer, primary_key=True) - answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id"), nullable=False) - tag = mapped_column(db.Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + answer_id: Mapped[int] = mapped_column(db.ForeignKey("answer.id")) + tag: Mapped[str] class AnswerUpload(db.Model): """Associates uploaded files (Block with type BlockType.AnswerUpload) with Answers.""" __tablename__ = "answerupload" - - upload_block_id = mapped_column( - db.Integer, db.ForeignKey("block.id"), primary_key=True + upload_block_id: Mapped[int] = mapped_column( + db.ForeignKey("block.id"), primary_key=True ) - answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id")) + answer_id: Mapped[int] = mapped_column(db.ForeignKey("answer.id")) - block = db.relationship("Block", back_populates="answerupload") - answer = db.relationship("Answer", back_populates="uploads") + block: Mapped["Block"] = db.relationship(back_populates="answerupload") + answer: Mapped["Answer"] = db.relationship(back_populates="uploads") def __init__(self, block, answer=None): self.block = block @@ -40,9 +44,9 @@ class UserAnswer(db.Model): """Associates Users with Answers.""" __tablename__ = "useranswer" - - id = mapped_column(db.Integer, primary_key=True) - answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id"), nullable=False) - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + answer_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("answer.id")) + user_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("useraccount.id")) + __table_args__ = (db.UniqueConstraint("answer_id", "user_id"),) diff --git a/timApp/auth/auth_models.py b/timApp/auth/auth_models.py index fdbad59d02..d3d1fcfbdf 100644 --- a/timApp/auth/auth_models.py +++ b/timApp/auth/auth_models.py @@ -1,14 +1,17 @@ from __future__ import annotations -from datetime import datetime -from typing import TYPE_CHECKING +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, Optional, List -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.auth.accesstype import AccessType +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.item.distribute_rights import Right + from timApp.item.block import Block + from timApp.user.usergroup import UserGroup from timApp.timdb.sqa import db, include_if_loaded from timApp.util.utils import get_current_time @@ -18,15 +21,14 @@ class AccessTypeModel(db.Model): """A kind of access that a UserGroup may have to a Block.""" __tablename__ = "accesstype" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """Access type identifier.""" - name = mapped_column(db.Text, nullable=False) + name: Mapped[Optional[str]] """Access type name, such as 'view', 'edit', 'manage', etc.""" - accesses = db.relationship("BlockAccess", back_populates="atype") + accesses: Mapped[List["BlockAccess"]] = db.relationship(back_populates="atype") def __json__(self): return ["id", "name"] @@ -39,23 +41,22 @@ class BlockAccess(db.Model): """A single permission. Relates a UserGroup with a Block along with an AccessType.""" __tablename__ = "blockaccess" - - block_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - usergroup_id = mapped_column( - db.Integer, db.ForeignKey("usergroup.id"), primary_key=True + block_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + usergroup_id: Mapped[int] = mapped_column( + db.ForeignKey("usergroup.id"), primary_key=True ) - type = mapped_column(db.Integer, db.ForeignKey("accesstype.id"), primary_key=True) - accessible_from = mapped_column(db.DateTime(timezone=True)) - accessible_to = mapped_column(db.DateTime(timezone=True)) - duration = mapped_column(db.Interval) - duration_from = mapped_column(db.DateTime(timezone=True)) - duration_to = mapped_column(db.DateTime(timezone=True)) - require_confirm = mapped_column(db.Boolean) - - block = db.relationship("Block", back_populates="accesses") - usergroup = db.relationship("UserGroup", back_populates="accesses") - atype = db.relationship("AccessTypeModel", back_populates="accesses") + type: Mapped[int] = mapped_column(db.ForeignKey("accesstype.id"), primary_key=True) + accessible_from: Mapped[Optional[datetime_tz]] + accessible_to: Mapped[Optional[datetime_tz]] + duration: Mapped[Optional[timedelta]] + duration_from: Mapped[Optional[datetime_tz]] + duration_to: Mapped[Optional[datetime_tz]] + require_confirm: Mapped[Optional[bool]] + + block: Mapped["Block"] = db.relationship(back_populates="accesses") + usergroup: Mapped["UserGroup"] = db.relationship(back_populates="accesses") + atype: Mapped["AccessTypeModel"] = db.relationship(back_populates="accesses") @property def duration_now(self): diff --git a/timApp/auth/oauth2/models.py b/timApp/auth/oauth2/models.py index 1c7096a780..925d894364 100644 --- a/timApp/auth/oauth2/models.py +++ b/timApp/auth/oauth2/models.py @@ -1,15 +1,22 @@ +import time from dataclasses import dataclass, field from enum import Enum - -from authlib.integrations.sqla_oauth2 import ( - OAuth2TokenMixin, - OAuth2AuthorizationCodeMixin, +from typing import TYPE_CHECKING, Optional + +from authlib.oauth2.rfc6749 import ( + ClientMixin, + scope_to_list, + list_to_scope, + TokenMixin, + AuthorizationCodeMixin, ) -from authlib.oauth2.rfc6749 import ClientMixin, scope_to_list, list_to_scope -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +if TYPE_CHECKING: + from timApp.user.user import User + class Scope(Enum): profile = "profile" @@ -101,19 +108,72 @@ def check_grant_type(self, grant_type: str) -> bool: return grant_type in self.grant_types -class OAuth2Token(db.Model, OAuth2TokenMixin): +class OAuth2Token(db.Model, TokenMixin): __tablename__ = "oauth2_token" - - id = mapped_column(db.Integer, primary_key=True) - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id")) - user = db.relationship("User") + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + user: Mapped["User"] = db.relationship() + + client_id: Mapped[Optional[str]] = mapped_column(db.String(48)) + token_type: Mapped[Optional[str]] = mapped_column(db.String(40)) + access_token: Mapped[str] = mapped_column(db.String(255), unique=True) + refresh_token: Mapped[Optional[str]] = mapped_column(db.String(255), index=True) + scope: Mapped[str] = mapped_column(default="") + issued_at: Mapped[int] = mapped_column(default=lambda: int(time.time())) + access_token_revoked_at: Mapped[int] = mapped_column(default=0) + refresh_token_revoked_at: Mapped[int] = mapped_column(default=0) + expires_in: Mapped[int] = mapped_column(default=0) + + def check_client(self, client): + return self.client_id == client.get_client_id() + + def get_scope(self): + return self.scope + + def get_expires_in(self): + return self.expires_in + + def is_revoked(self): + return self.access_token_revoked_at or self.refresh_token_revoked_at + + def is_expired(self): + if not self.expires_in: + return False + expires_at = self.issued_at + self.expires_in + return expires_at < time.time() -class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): + +class OAuth2AuthorizationCode(db.Model, AuthorizationCodeMixin): __tablename__ = "oauth2_auth_code" - - id = mapped_column(db.Integer, primary_key=True) - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id")) - user = db.relationship("User") + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + user: Mapped["User"] = db.relationship() + + code: Mapped[str] = mapped_column(db.String(120), unique=True) + client_id: Mapped[str] = mapped_column(db.String(48)) + redirect_uri: Mapped[str] = mapped_column(default="") + response_type: Mapped[str] = mapped_column(default="") + scope: Mapped[str] = mapped_column(default="") + nonce: Mapped[Optional[str]] + auth_time: Mapped[int] = mapped_column(default=lambda: int(time.time())) + + code_challenge: Mapped[Optional[str]] + code_challenge_method: Mapped[Optional[str]] = mapped_column(db.String(48)) + + def is_expired(self): + return self.auth_time + 300 < time.time() + + def get_redirect_uri(self): + return self.redirect_uri + + def get_scope(self): + return self.scope + + def get_auth_time(self): + return self.auth_time + + def get_nonce(self): + return self.nonce diff --git a/timApp/auth/session/model.py b/timApp/auth/session/model.py index e1b198b6ed..38cd30d81f 100755 --- a/timApp/auth/session/model.py +++ b/timApp/auth/session/model.py @@ -1,13 +1,18 @@ """ Database models for session management. """ +from datetime import datetime +from typing import Optional, TYPE_CHECKING from sqlalchemy.ext.hybrid import hybrid_property # type: ignore -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db from timApp.util.utils import get_current_time +if TYPE_CHECKING: + from timApp.user.user import User + class UserSession(db.Model): """ @@ -21,35 +26,35 @@ class UserSession(db.Model): __tablename__ = "usersession" - user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + user_id: Mapped[int] = mapped_column( + db.ForeignKey("useraccount.id"), primary_key=True ) """ User ID of the user who owns the session. """ - session_id = mapped_column(db.Text, primary_key=True) + session_id: Mapped[str] = mapped_column(primary_key=True) """ Unique session ID. """ - logged_in_at = mapped_column(db.DateTime, nullable=False, default=get_current_time) + logged_in_at: Mapped[datetime] = mapped_column(default=get_current_time) """ The time when the user logged in and the session was created. """ - expired_at = mapped_column(db.DateTime, nullable=True) + expired_at: Mapped[Optional[datetime]] """ The time when the session was expired. """ - origin = mapped_column(db.Text, nullable=False) + origin: Mapped[str] """ Information about the origin of the session. May include user agent and any other information about login state. """ - user = db.relationship("User", back_populates="sessions") + user: Mapped["User"] = db.relationship("User", back_populates="sessions") """ User that owns the session. Relationship to :attr:`user_id`. """ diff --git a/timApp/celery_sqlalchemy_scheduler/models.py b/timApp/celery_sqlalchemy_scheduler/models.py index d4f6275b03..dcc1f13073 100644 --- a/timApp/celery_sqlalchemy_scheduler/models.py +++ b/timApp/celery_sqlalchemy_scheduler/models.py @@ -1,4 +1,5 @@ import datetime as dt +from typing import Optional import pytz import sqlalchemy as sa @@ -6,13 +7,14 @@ from celery.utils.log import get_logger from sqlalchemy import func from sqlalchemy.event import listen -from sqlalchemy.orm import relationship, foreign, remote, mapped_column +from sqlalchemy.orm import relationship, mapped_column, Mapped from sqlalchemy.sql import select, insert, update from .session import ModelBase from .tzcrontab import TzAwareCrontab from ..item.block import Block from ..plugin.taskid import TaskId +from ..timdb.types import datetime_tz from ..util.utils import cached_property logger = get_logger("celery_sqlalchemy_scheduler.models") @@ -45,10 +47,10 @@ class IntervalSchedule(ModelBase, ModelMixin): SECONDS = "seconds" MICROSECONDS = "microseconds" - id = mapped_column(sa.Integer, primary_key=True, autoincrement=True) + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - every = mapped_column(sa.Integer, nullable=False) - period = mapped_column(sa.String(24)) + every: Mapped[int] + period: Mapped[str] = mapped_column(sa.String(24)) def __repr__(self): if self.every == 1: @@ -89,13 +91,13 @@ class CrontabSchedule(ModelBase, ModelMixin): __table_args__ = {"sqlite_autoincrement": True} - id = mapped_column(sa.Integer, primary_key=True, autoincrement=True) - minute = mapped_column(sa.String(60 * 4), default="*") - hour = mapped_column(sa.String(24 * 4), default="*") - day_of_week = mapped_column(sa.String(64), default="*") - day_of_month = mapped_column(sa.String(31 * 4), default="*") - month_of_year = mapped_column(sa.String(64), default="*") - timezone = mapped_column(sa.String(64), default="UTC") + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + minute: Mapped[str] = mapped_column(sa.String(60 * 4), default="*") + hour: Mapped[str] = mapped_column(sa.String(24 * 4), default="*") + day_of_week: Mapped[str] = mapped_column(sa.String(64), default="*") + day_of_month: Mapped[str] = mapped_column(sa.String(31 * 4), default="*") + month_of_year: Mapped[str] = mapped_column(sa.String(64), default="*") + timezone: Mapped[str] = mapped_column(sa.String(64), default="UTC") def __repr__(self): return "{} {} {} {} {} (m/h/d/dM/MY) {}".format( @@ -144,13 +146,11 @@ def from_schedule(cls, session, schedule): class SolarSchedule(ModelBase, ModelMixin): __tablename__ = "celery_solar_schedule" __table_args__ = {"sqlite_autoincrement": True} - - - id = mapped_column(sa.Integer, primary_key=True, autoincrement=True) - event = mapped_column(sa.String(24)) - latitude = mapped_column(sa.Float()) - longitude = mapped_column(sa.Float()) + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + event: Mapped[Optional[str]] = mapped_column(sa.String(24)) + latitude: Mapped[Optional[float]] + longitude: Mapped[Optional[float]] @property def schedule(self): @@ -184,10 +184,8 @@ class PeriodicTaskChanged(ModelBase, ModelMixin): __tablename__ = "celery_periodic_task_changed" - id = mapped_column(sa.Integer, primary_key=True) - last_update = mapped_column( - sa.DateTime(timezone=True), nullable=False, default=dt.datetime.now - ) + id: Mapped[int] = mapped_column(primary_key=True) + last_update: Mapped[datetime_tz] = mapped_column(default=dt.datetime.now) @classmethod def changed(cls, mapper, connection, target): @@ -234,60 +232,51 @@ def last_change(cls, session): class PeriodicTask(ModelBase, ModelMixin): __tablename__ = "celery_periodic_task" __table_args__ = {"sqlite_autoincrement": True} - - id = mapped_column(sa.Integer, primary_key=True, autoincrement=True) - block_id = mapped_column(sa.Integer, sa.ForeignKey("block.id"), nullable=True) - block = relationship(Block) + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + block_id: Mapped[Optional[int]] = mapped_column(sa.ForeignKey("block.id")) + block: Mapped[Block] = relationship(Block) # name - name = mapped_column(sa.String(255), unique=True) + name: Mapped[str] = mapped_column(sa.String(255), unique=True) # task name - task = mapped_column(sa.String(255)) + task: Mapped[str] = mapped_column(sa.String(255)) # not use ForeignKey - interval_id = mapped_column(sa.Integer) - interval = relationship( - IntervalSchedule, - uselist=False, - primaryjoin=foreign(interval_id) == remote(IntervalSchedule.id), + interval_id: Mapped[Optional[int]] + interval: Mapped[Optional[IntervalSchedule]] = relationship( + primaryjoin="foreign(PeriodicTask.interval_id) == remote(IntervalSchedule.id)", ) - crontab_id = mapped_column(sa.Integer) - crontab = relationship( - CrontabSchedule, - uselist=False, - primaryjoin=foreign(crontab_id) == remote(CrontabSchedule.id), + crontab_id: Mapped[Optional[int]] + crontab: Mapped[Optional[CrontabSchedule]] = relationship( + primaryjoin="foreign(PeriodicTask.crontab_id) == remote(CrontabSchedule.id)", ) - solar_id = mapped_column(sa.Integer) - solar = relationship( - SolarSchedule, - uselist=False, - primaryjoin=foreign(solar_id) == remote(SolarSchedule.id), + solar_id: Mapped[Optional[int]] + solar: Mapped[Optional[SolarSchedule]] = relationship( + primaryjoin="foreign(PeriodicTask.solar_id) == remote(SolarSchedule.id)", ) - args = mapped_column(sa.Text(), default="[]") - kwargs = mapped_column(sa.Text(), default="{}") + args: Mapped[str] = mapped_column(default="[]") + kwargs: Mapped[str] = mapped_column(default="{}") # queue for celery - queue = mapped_column(sa.String(255)) + queue: Mapped[Optional[str]] = mapped_column(sa.String(255)) # exchange for celery - exchange = mapped_column(sa.String(255)) + exchange: Mapped[Optional[str]] = mapped_column(sa.String(255)) # routing_key for celery - routing_key = mapped_column(sa.String(255)) - priority = mapped_column(sa.Integer()) - expires = mapped_column(sa.DateTime(timezone=True)) + routing_key: Mapped[Optional[str]] = mapped_column(sa.String(255)) + priority: Mapped[Optional[int]] + expires: Mapped[Optional[datetime_tz]] # 只执行一次 - one_off = mapped_column(sa.Boolean(), default=False) - start_time = mapped_column(sa.DateTime(timezone=True)) - enabled = mapped_column(sa.Boolean(), default=True) - last_run_at = mapped_column(sa.DateTime(timezone=True)) - total_run_count = mapped_column(sa.Integer(), nullable=False, default=0) + one_off: Mapped[bool] = mapped_column(default=False) + start_time: Mapped[Optional[datetime_tz]] + enabled: Mapped[bool] = mapped_column(default=True) + last_run_at: Mapped[Optional[datetime_tz]] + total_run_count: Mapped[int] = mapped_column(default=0) # 修改时间 - date_changed = mapped_column( - sa.DateTime(timezone=True), default=func.now(), onupdate=func.now() - ) - description = mapped_column(sa.Text(), default="") + date_changed: Mapped[datetime_tz] = mapped_column(default=func.now(), onupdate=func.now()) + description: Mapped[str] = mapped_column(default="") no_changes = False diff --git a/timApp/document/docentry.py b/timApp/document/docentry.py index 702dedbfc3..81188ff802 100644 --- a/timApp/document/docentry.py +++ b/timApp/document/docentry.py @@ -1,15 +1,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, List from sqlalchemy import select -from sqlalchemy.orm import foreign, mapped_column +from sqlalchemy.orm import foreign, mapped_column, Mapped from timApp.document.docinfo import DocInfo from timApp.document.document import Document from timApp.document.translation.translation import Translation from timApp.folder.createopts import FolderCreationOptions -from timApp.item.block import BlockType +from timApp.item.block import BlockType, Block from timApp.item.block import insert_block from timApp.timdb.exceptions import ItemAlreadyExistsException from timApp.timdb.sqa import db @@ -31,22 +31,21 @@ class DocEntry(db.Model, DocInfo): __tablename__ = "docentry" - name = mapped_column(db.Text, primary_key=True) + name: Mapped[str] = mapped_column(primary_key=True) """Full path of the document. TODO: Improve the name. """ - id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) + id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) """Document identifier.""" - public = mapped_column(db.Boolean, nullable=False, default=True) + public: Mapped[bool] = mapped_column(default=True) """Whether the document is visible in directory listing.""" - _block = db.relationship("Block", back_populates="docentries", lazy="joined") + _block: Mapped["Block"] = db.relationship(back_populates="docentries", lazy="joined") - trs = db.relationship( - "Translation", + trs: Mapped[List[Translation]] = db.relationship( primaryjoin=id == foreign(Translation.src_docid), back_populates="docentry", # When a DocEntry object is deleted, we don't want to touch the translation objects at all. @@ -54,8 +53,7 @@ class DocEntry(db.Model, DocInfo): # TODO: This feels slightly hacky. This relationship attribute might be better in Block class, although that # doesn't sound ideal either. passive_deletes="all", - cascade_backrefs=False, - ) # : list[Translation] + ) __table_args__ = (db.Index("docentry_id_idx", "id"),) diff --git a/timApp/document/translation/language.py b/timApp/document/translation/language.py index b12b771ffc..c2dbab50f7 100644 --- a/timApp/document/translation/language.py +++ b/timApp/document/translation/language.py @@ -13,18 +13,15 @@ __license__ = "MIT" __date__ = "25.4.2022" - -from dataclasses import dataclass from typing import Optional import langcodes from sqlalchemy import select -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db -@dataclass class Language(db.Model): """Represents a standardized language code used for example with translation documents. @@ -34,19 +31,18 @@ class Language(db.Model): """ __tablename__ = "language" - - lang_code = mapped_column(db.Text, nullable=False, primary_key=True) + lang_code: Mapped[str] = mapped_column(primary_key=True) """Standardized code of the language.""" # TODO should this be unique? - lang_name = mapped_column(db.Text, nullable=False) + lang_name: Mapped[str] """IANA's name for the language.""" - flag_uri = mapped_column(db.Text) + flag_uri: Mapped[Optional[str]] """Path to a picture representing the language.""" - autonym = mapped_column(db.Text, nullable=False) + autonym: Mapped[str] """Native name for the language.""" # FIXME: Turn into postinit diff --git a/timApp/document/translation/translation.py b/timApp/document/translation/translation.py index 2c39ed0db2..d922b83ae0 100644 --- a/timApp/document/translation/translation.py +++ b/timApp/document/translation/translation.py @@ -1,9 +1,15 @@ +from typing import TYPE_CHECKING + from sqlalchemy import UniqueConstraint -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.document.docinfo import DocInfo from timApp.timdb.sqa import db +if TYPE_CHECKING: + from timApp.item.block import Block + from timApp.document.docentry import DocEntry + class Translation(db.Model, DocInfo): """A translated document. @@ -18,17 +24,16 @@ class Translation(db.Model, DocInfo): __tablename__ = "translation" - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - src_docid = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) - lang_id = mapped_column(db.Text, nullable=False) + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + src_docid: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + lang_id: Mapped[str] __table_args__ = (UniqueConstraint("src_docid", "lang_id", name="translation_uc"),) - _block = db.relationship( + _block: Mapped["Block"] = db.relationship( "Block", back_populates="translation", foreign_keys=[doc_id] ) - docentry = db.relationship( - "DocEntry", + docentry: Mapped["DocEntry"] = db.relationship( back_populates="trs", primaryjoin="foreign(Translation.src_docid) == DocEntry.id", ) diff --git a/timApp/document/translation/translator.py b/timApp/document/translation/translator.py index fdbf35a0b8..7beca98475 100644 --- a/timApp/document/translation/translator.py +++ b/timApp/document/translation/translator.py @@ -22,7 +22,7 @@ import pypandoc from sqlalchemy import select -from sqlalchemy.orm import with_polymorphic, mapped_column +from sqlalchemy.orm import with_polymorphic, mapped_column, Mapped from timApp.document.docparagraph import DocParagraph from timApp.document.translation.language import Language @@ -77,10 +77,10 @@ class TranslationService(db.Model): __tablename__ = "translationservice" - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """Translation service identifier.""" - service_name = mapped_column(db.Text, unique=True, nullable=False) + service_name: Mapped[str] = mapped_column(unique=True) """Human-readable name of the machine translator. Also used as an identifier.""" @@ -181,23 +181,19 @@ class TranslationServiceKey(db.Model): __tablename__ = "translationservicekey" - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """Key identifier.""" # TODO Come up with a better name? - api_key = mapped_column(db.Text, nullable=False) + api_key: Mapped[str] """The key needed for using related service.""" - group_id = mapped_column(db.Integer, db.ForeignKey("usergroup.id"), nullable=False) - group = db.relationship("UserGroup", uselist=False) # : UserGroup + group_id: Mapped[int] = mapped_column(db.ForeignKey("usergroup.id")) + group: Mapped[UserGroup] = db.relationship() """The group that can use this key.""" - service_id = mapped_column( - db.Integer, - db.ForeignKey("translationservice.id"), - nullable=False, - ) - service = db.relationship("TranslationService", uselist=False) # : TranslationService + service_id: Mapped[int] = mapped_column(db.ForeignKey("translationservice.id")) + service: Mapped[TranslationService] = db.relationship() """The service that this key is used in.""" @staticmethod diff --git a/timApp/folder/folder.py b/timApp/folder/folder.py index d0c7a8b8ba..052f9dda96 100644 --- a/timApp/folder/folder.py +++ b/timApp/folder/folder.py @@ -3,7 +3,7 @@ from typing import Iterable, Any, TYPE_CHECKING from sqlalchemy import true, and_, select, delete -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.auth.auth_models import BlockAccess from timApp.document.docentry import DocEntry, get_documents @@ -27,20 +27,19 @@ class Folder(db.Model, Item): """Represents a folder in the directory hierarchy.""" __tablename__ = "folder" - - id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) """Folder identifier.""" - name = mapped_column(db.Text, nullable=False) + name: Mapped[str] """Folder name (last part of path).""" - location = mapped_column(db.Text, nullable=False) + location: Mapped[str] """Folder location (first parts of the path).""" __table_args__ = (db.UniqueConstraint("name", "location", name="folder_uc"),) - _block = db.relationship("Block", back_populates="folder", lazy="joined") + _block: Mapped[Block] = db.relationship(back_populates="folder", lazy="joined") @staticmethod def get_root() -> Folder: diff --git a/timApp/item/block.py b/timApp/item/block.py index a457b393b6..455013882d 100644 --- a/timApp/item/block.py +++ b/timApp/item/block.py @@ -1,117 +1,116 @@ from __future__ import annotations from enum import Enum -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, List, Dict, Tuple from sqlalchemy import func -from sqlalchemy.orm import mapped_column -from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.orm import mapped_column, Mapped, attribute_keyed_dict, DynamicMapped from timApp.auth.accesstype import AccessType from timApp.auth.auth_models import BlockAccess from timApp.item.blockassociation import BlockAssociation from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.user.usergroup import UserGroup from timApp.user.usergroupdoc import UserGroupDoc from timApp.util.utils import get_current_time if TYPE_CHECKING: from timApp.folder.folder import Folder + from timApp.document.docentry import DocEntry + from timApp.document.translation.translation import Translation + from timApp.answer.answer_models import AnswerUpload + from timApp.item.tag import Tag + from timApp.notification.notification import Notification + from timApp.item.blockrelevance import BlockRelevance + from timApp.messaging.messagelist.messagelist_models import MessageListModel + from timApp.messaging.timMessage.internalmessage_models import InternalMessage, InternalMessageDisplay class Block(db.Model): """The "base class" for all database objects that are part of the permission system.""" __tablename__ = "block" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """A unique identifier for the Block.""" - latest_revision_id = mapped_column(db.Integer) + latest_revision_id: Mapped[Optional[int]] """Old field that is not used anymore.""" - type_id = mapped_column(db.Integer, nullable=False) + type_id: Mapped[int] """Type of the Block, see BlockType enum for possible types.""" - description = mapped_column(db.Text) + description: Mapped[Optional[str]] """Additional information about the Block. This is used for different purposes by different BlockTypes, so it isn't merely a "description". """ - created = mapped_column(db.DateTime(timezone=True), nullable=False, default=func.now()) + created: Mapped[datetime_tz] = mapped_column(default=func.now()) """When this Block was created.""" - modified = mapped_column(db.DateTime(timezone=True), default=func.now()) + modified: Mapped[Optional[datetime_tz]] = mapped_column(default=func.now()) """When this Block was last modified.""" - docentries = db.relationship("DocEntry", back_populates="_block") - folder = db.relationship( - "Folder", back_populates="_block", uselist=False, cascade_backrefs=False + docentries: Mapped[List["DocEntry"]] = db.relationship(back_populates="_block") + folder: Mapped[Optional[Folder]] = db.relationship( + back_populates="_block" ) - translation = db.relationship( + translation: Mapped[Optional["Translation"]] = db.relationship( "Translation", back_populates="_block", - uselist=False, - foreign_keys="Translation.doc_id", - cascade_backrefs=False, + foreign_keys="Translation.doc_id" ) - answerupload = db.relationship( - "AnswerUpload", back_populates="block", lazy="dynamic", cascade_backrefs=False + answerupload: DynamicMapped[Optional["AnswerUpload"]] = db.relationship( + back_populates="block", lazy="dynamic" ) - accesses = db.relationship( - "BlockAccess", + accesses: Mapped[Dict[Tuple[int, int], "BlockAccess"]] = db.relationship( back_populates="block", lazy="selectin", cascade="all, delete-orphan", - collection_class=attribute_mapped_collection("block_collection_key"), - cascade_backrefs=False, + collection_class=attribute_keyed_dict("block_collection_key"), ) - tags = db.relationship("Tag", back_populates="block", lazy="select") # : list[Tag] - children = db.relationship( - "Block", + tags: Mapped[List["Tag"]] = db.relationship("Tag", back_populates="block", lazy="select") + children: Mapped[List["Block"]] = db.relationship( secondary=BlockAssociation.__table__, primaryjoin=id == BlockAssociation.__table__.c.parent, secondaryjoin=id == BlockAssociation.__table__.c.child, lazy="select", ) - parents = db.relationship( - "Block", + parents: Mapped[List["Block"]] = db.relationship( secondary=BlockAssociation.__table__, primaryjoin=id == BlockAssociation.__table__.c.child, secondaryjoin=id == BlockAssociation.__table__.c.parent, lazy="select", overlaps="children", ) - notifications = db.relationship( - "Notification", back_populates="block", lazy="dynamic" + notifications: DynamicMapped["Notification"] = db.relationship( + back_populates="block", lazy="dynamic" ) - relevance = db.relationship( - "BlockRelevance", back_populates="_block", uselist=False + relevance: Mapped[Optional["BlockRelevance"]] = db.relationship( + back_populates="_block" ) # If this Block corresponds to a group's manage document, indicates the group being managed. - managed_usergroup = db.relationship( - "UserGroup", + managed_usergroup: Mapped[Optional[UserGroup]] = db.relationship( secondary=UserGroupDoc.__table__, lazy="select", - uselist=False, overlaps="admin_doc", - ) # : UserGroup | None + ) # If this Block corresponds to a message list's manage document, indicates the message list # being managed. - managed_messagelist = db.relationship( - "MessageListModel", back_populates="block", lazy="select" - ) # : MessageListModel | None - - internalmessage = db.relationship( - "InternalMessage", back_populates="block", cascade_backrefs=False - ) # : InternalMessage | None - internalmessage_display = db.relationship( - "InternalMessageDisplay", back_populates="display_block", cascade_backrefs=False - ) # : InternalMessageDisplay | None + managed_messagelist: Mapped[Optional["MessageListModel"]] = db.relationship( + back_populates="block", lazy="select" + ) + + internalmessage: Mapped[Optional["InternalMessage"]] = db.relationship( + back_populates="block" + ) + internalmessage_display: Mapped[Optional["InternalMessageDisplay"]] = db.relationship( + back_populates="display_block" + ) def __json__(self): return ["id", "type_id", "description", "created", "modified"] diff --git a/timApp/item/blockassociation.py b/timApp/item/blockassociation.py index 77bc4bd276..33a550e89d 100644 --- a/timApp/item/blockassociation.py +++ b/timApp/item/blockassociation.py @@ -1,4 +1,4 @@ -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db @@ -7,10 +7,9 @@ class BlockAssociation(db.Model): """Associates blocks with other blocks. Currently only used for associating uploaded files with documents.""" __tablename__ = "blockassociation" - - parent = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + parent: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) """The parent Block.""" - child = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + child: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) """The child Block.""" diff --git a/timApp/item/blockrelevance.py b/timApp/item/blockrelevance.py index 084bfbadea..63f7c5fcca 100644 --- a/timApp/item/blockrelevance.py +++ b/timApp/item/blockrelevance.py @@ -1,15 +1,18 @@ -from sqlalchemy.orm import mapped_column +from typing import TYPE_CHECKING + +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +if TYPE_CHECKING: + from timApp.item.block import Block class BlockRelevance(db.Model): """A relevance value of a block (used in search).""" __tablename__ = "blockrelevance" - - block_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - relevance = mapped_column(db.Integer, nullable=False) + block_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + relevance: Mapped[int] - _block = db.relationship("Block", back_populates="relevance") + _block: Mapped["Block"] = db.relationship(back_populates="relevance") diff --git a/timApp/item/tag.py b/timApp/item/tag.py index 44c531c4fb..2048f7a3cc 100644 --- a/timApp/item/tag.py +++ b/timApp/item/tag.py @@ -1,8 +1,13 @@ from enum import Enum, unique +from typing import Optional, TYPE_CHECKING -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz + +if TYPE_CHECKING: + from timApp.item.block import Block @unique @@ -23,14 +28,13 @@ class Tag(db.Model): """A tag with associated document id, tag name, type and expiration date.""" __tablename__ = "tag" - - block_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - name = mapped_column(db.Text, primary_key=True) - type = mapped_column(db.Enum(TagType), nullable=False) - expires = mapped_column(db.DateTime(timezone=True)) + block_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + name: Mapped[str] = mapped_column(primary_key=True) + type: Mapped[TagType] + expires: Mapped[Optional[datetime_tz]] - block = db.relationship("Block", back_populates="tags") + block: Mapped["Block"] = db.relationship(back_populates="tags") def __json__(self): return ["block_id", "name", "type", "expires"] diff --git a/timApp/item/taskblock.py b/timApp/item/taskblock.py index 23caf2f08a..b2f7f4fd86 100644 --- a/timApp/item/taskblock.py +++ b/timApp/item/taskblock.py @@ -1,7 +1,7 @@ from __future__ import annotations from sqlalchemy import select -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.item.block import Block, BlockType, insert_block from timApp.timdb.sqa import db @@ -10,12 +10,11 @@ class TaskBlock(db.Model): __tablename__ = "taskblock" - - id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - task_id = mapped_column(db.Text, primary_key=True) + id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + task_id: Mapped[str] = mapped_column(primary_key=True) - block = db.relationship("Block", lazy="selectin") + block: Mapped[Block] = db.relationship(lazy="select") @staticmethod def get_by_task(task_id: str) -> TaskBlock | None: diff --git a/timApp/lecture/askedjson.py b/timApp/lecture/askedjson.py index ebf4ab13c1..c47b50ae14 100644 --- a/timApp/lecture/askedjson.py +++ b/timApp/lecture/askedjson.py @@ -3,18 +3,17 @@ from typing import Any from sqlalchemy import select -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db class AskedJson(db.Model): __tablename__ = "askedjson" - - - asked_json_id = mapped_column(db.Integer, primary_key=True) - json = mapped_column(db.Text, nullable=False) - hash = mapped_column(db.Text, nullable=False) + + asked_json_id: Mapped[int] = mapped_column(primary_key=True) + json: Mapped[str] + hash: Mapped[str] asked_questions = db.relationship( "AskedQuestion", back_populates="asked_json", lazy="selectin" diff --git a/timApp/lecture/askedquestion.py b/timApp/lecture/askedquestion.py index bd825ab363..da9eeed124 100644 --- a/timApp/lecture/askedquestion.py +++ b/timApp/lecture/askedquestion.py @@ -1,54 +1,58 @@ import json from contextlib import contextmanager from datetime import timedelta, datetime +from typing import Optional, TYPE_CHECKING, List from sqlalchemy import func, select -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, DynamicMapped from timApp.lecture.question_utils import qst_rand_array, qst_filter_markup_points from timApp.lecture.questionactivity import QuestionActivityKind, QuestionActivity from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.timtypes import UserType from timApp.util.utils import get_current_time +if TYPE_CHECKING: + from timApp.lecture.askedjson import AskedJson + from timApp.lecture.lecture import Lecture + from timApp.lecture.lectureanswer import LectureAnswer + from timApp.lecture.runningquestion import Runningquestion + from timApp.lecture.showpoints import Showpoints + class AskedQuestion(db.Model): __tablename__ = "askedquestion" - - - asked_id = mapped_column(db.Integer, primary_key=True) - lecture_id = mapped_column( - db.Integer, db.ForeignKey("lecture.lecture_id"), nullable=False + + asked_id: Mapped[int] = mapped_column(primary_key=True) + lecture_id: Mapped[int] = mapped_column(db.ForeignKey("lecture.lecture_id")) + doc_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) + par_id: Mapped[Optional[str]] + asked_time: Mapped[datetime_tz] + points: Mapped[Optional[str]] + asked_json_id: Mapped[int] = mapped_column(db.ForeignKey("askedjson.asked_json_id")) + expl: Mapped[Optional[str]] + + asked_json: Mapped["AskedJson"] = db.relationship( + back_populates="asked_questions", lazy="selectin" ) - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id")) - par_id = mapped_column(db.Text) - asked_time = mapped_column(db.DateTime(timezone=True), nullable=False) - points = mapped_column(db.Text) # not a single number; cannot be numeric - asked_json_id = mapped_column( - db.Integer, db.ForeignKey("askedjson.asked_json_id"), nullable=False + lecture: Mapped["Lecture"] = db.relationship( + back_populates="asked_questions", lazy="selectin" ) - expl = mapped_column(db.Text) - - asked_json = db.relationship( - "AskedJson", back_populates="asked_questions", lazy="selectin" - ) # : AskedJson - lecture = db.relationship( - "Lecture", back_populates="asked_questions", lazy="selectin" - ) # : Lecture - answers = db.relationship( - "LectureAnswer", back_populates="asked_question", lazy="dynamic" + answers: DynamicMapped["LectureAnswer"] = db.relationship( + back_populates="asked_question", lazy="dynamic" ) - answers_all = db.relationship( - "LectureAnswer", back_populates="asked_question", overlaps="answers" + answers_all: Mapped[List["LectureAnswer"]] = db.relationship( + back_populates="asked_question", overlaps="answers" ) - running_question = db.relationship( - "Runningquestion", back_populates="asked_question", lazy="select", uselist=False, cascade_backrefs=False + running_question: Mapped[Optional["Runningquestion"]] = db.relationship( + back_populates="asked_question", lazy="select" ) - questionactivity = db.relationship( - "QuestionActivity", back_populates="asked_question", lazy="dynamic", cascade_backrefs=False + questionactivity: DynamicMapped["QuestionActivity"] = db.relationship( + back_populates="asked_question", lazy="dynamic" ) - showpoints = db.relationship( - "Showpoints", back_populates="asked_question", lazy="select", cascade_backrefs=False + showpoints: Mapped[Optional["Showpoints"]] = db.relationship( + "Showpoints", back_populates="asked_question", lazy="select" ) @property diff --git a/timApp/lecture/lecture.py b/timApp/lecture/lecture.py index 7d4809acda..5ef87c97c0 100644 --- a/timApp/lecture/lecture.py +++ b/timApp/lecture/lecture.py @@ -1,53 +1,52 @@ import json from datetime import datetime, timezone -from typing import Optional +from typing import Optional, TYPE_CHECKING, List from sqlalchemy import select, func -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, DynamicMapped from timApp.lecture.lectureusers import LectureUsers from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.util.utils import get_current_time +if TYPE_CHECKING: + from timApp.user.user import User + from timApp.lecture.askedquestion import AskedQuestion + from timApp.lecture.message import Message + from timApp.lecture.runningquestion import Runningquestion + from timApp.lecture.useractivity import Useractivity class Lecture(db.Model): __tablename__ = "lecture" - - lecture_id = mapped_column(db.Integer, primary_key=True) - lecture_code = mapped_column(db.Text) - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) - lecturer = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), nullable=False - ) - start_time = mapped_column(db.DateTime(timezone=True), nullable=False) - end_time = mapped_column(db.DateTime(timezone=True)) - password = mapped_column(db.Text) - options = mapped_column(db.Text) + lecture_id: Mapped[int] = mapped_column(primary_key=True) + lecture_code: Mapped[Optional[str]] + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + lecturer: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + start_time: Mapped[datetime_tz] + end_time: Mapped[Optional[datetime_tz]] + password: Mapped[Optional[str]] + options: Mapped[Optional[str]] - users = db.relationship( - "User", + users: DynamicMapped["User"] = db.relationship( secondary=LectureUsers.__table__, back_populates="lectures", lazy="dynamic", ) - asked_questions = db.relationship( - "AskedQuestion", + asked_questions: DynamicMapped["AskedQuestion"] = db.relationship( back_populates="lecture", lazy="dynamic", - cascade_backrefs=False, ) - messages = db.relationship("Message", back_populates="lecture", lazy="dynamic") - running_questions = db.relationship( - "Runningquestion", + messages: DynamicMapped["Message"] = db.relationship(back_populates="lecture", lazy="dynamic") + running_questions: Mapped[List["Runningquestion"]] = db.relationship( back_populates="lecture", lazy="select", - cascade_backrefs=False, ) - useractivity = db.relationship( - "Useractivity", back_populates="lecture", lazy="select" + useractivity: Mapped[List["Useractivity"]] = db.relationship( + back_populates="lecture", lazy="select" ) - owner = db.relationship("User", back_populates="owned_lectures") + owner: Mapped["User"] = db.relationship(back_populates="owned_lectures") @staticmethod def find_by_id(lecture_id: int) -> Optional["Lecture"]: diff --git a/timApp/lecture/lectureanswer.py b/timApp/lecture/lectureanswer.py index 71253b3e68..e42febb028 100644 --- a/timApp/lecture/lectureanswer.py +++ b/timApp/lecture/lectureanswer.py @@ -1,14 +1,18 @@ import json from json import JSONDecodeError -from typing import Optional +from typing import Optional, TYPE_CHECKING from sqlalchemy import func, select -from sqlalchemy.orm import lazyload, mapped_column +from sqlalchemy.orm import lazyload, mapped_column, Mapped from timApp.lecture.lecture import Lecture from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.user.user import User +if TYPE_CHECKING: + from timApp.lecture.askedquestion import AskedQuestion + def unshuffle_lectureanswer( answer: list[list[str]], question_type: str, row_count: int, rand_arr: list[int] @@ -26,24 +30,17 @@ def unshuffle_lectureanswer( class LectureAnswer(db.Model): __tablename__ = "lectureanswer" - - answer_id = mapped_column(db.Integer, primary_key=True) - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) - question_id = mapped_column( - db.Integer, db.ForeignKey("askedquestion.asked_id"), nullable=False - ) - lecture_id = mapped_column( - db.Integer, db.ForeignKey("lecture.lecture_id"), nullable=False - ) - answer = mapped_column(db.Text, nullable=False) - answered_on = mapped_column(db.DateTime(timezone=True), nullable=False) - points = mapped_column(db.Float) + answer_id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + question_id: Mapped[int] = mapped_column(db.ForeignKey("askedquestion.asked_id")) + lecture_id: Mapped[int] = mapped_column(db.ForeignKey("lecture.lecture_id")) + answer: Mapped[str] + answered_on: Mapped[datetime_tz] + points: Mapped[Optional[float]] - asked_question = db.relationship( - "AskedQuestion", back_populates="answers", lazy="selectin" - ) - user = db.relationship("User", back_populates="lectureanswers", lazy="selectin") + asked_question: Mapped["AskedQuestion"] = db.relationship(back_populates="answers", lazy="selectin") + user: Mapped["User"] = db.relationship(back_populates="lectureanswers", lazy="selectin") @staticmethod def get_by_id(ans_id: int) -> Optional["LectureAnswer"]: diff --git a/timApp/lecture/lectureusers.py b/timApp/lecture/lectureusers.py index cc19d86da0..ff5eed6f72 100644 --- a/timApp/lecture/lectureusers.py +++ b/timApp/lecture/lectureusers.py @@ -1,13 +1,10 @@ -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db class LectureUsers(db.Model): __tablename__ = "lectureusers" - - - lecture_id = mapped_column( - db.Integer, db.ForeignKey("lecture.lecture_id"), primary_key=True - ) - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) + + lecture_id: Mapped[int] = mapped_column(db.ForeignKey("lecture.lecture_id"), primary_key=True) + user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id"), primary_key=True) diff --git a/timApp/lecture/message.py b/timApp/lecture/message.py index ad119cd1cb..338acbaeca 100644 --- a/timApp/lecture/message.py +++ b/timApp/lecture/message.py @@ -1,26 +1,26 @@ from datetime import datetime +from typing import TYPE_CHECKING -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz +if TYPE_CHECKING: + from timApp.lecture.lecture import Lecture + from timApp.user.user import User class Message(db.Model): __tablename__ = "message" - - msg_id = mapped_column(db.Integer, primary_key=True) - lecture_id = mapped_column( - db.Integer, db.ForeignKey("lecture.lecture_id"), nullable=False - ) - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) - message = mapped_column(db.Text, nullable=False) - timestamp = mapped_column( - db.DateTime(timezone=True), nullable=False, default=datetime.utcnow - ) + msg_id: Mapped[int] = mapped_column(primary_key=True) + lecture_id: Mapped[int] = mapped_column(db.ForeignKey("lecture.lecture_id")) + user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + message: Mapped[str] + timestamp: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) - lecture = db.relationship("Lecture", back_populates="messages", lazy="select") - user = db.relationship("User", back_populates="messages", lazy="select") + lecture: Mapped["Lecture"] = db.relationship(back_populates="messages", lazy="select") + user: Mapped["User"] = db.relationship(back_populates="messages", lazy="select") def to_json(self): return { diff --git a/timApp/lecture/question.py b/timApp/lecture/question.py index 4de53ab8b5..760e6805d5 100644 --- a/timApp/lecture/question.py +++ b/timApp/lecture/question.py @@ -1,17 +1,18 @@ -from sqlalchemy.orm import mapped_column +from typing import Optional + +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db class Question(db.Model): __tablename__ = "question" - - question_id = mapped_column(db.Integer, primary_key=True) - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) - par_id = mapped_column(db.Text, nullable=False) - question_title = mapped_column(db.Text, nullable=False) - answer = mapped_column(db.Text) - questionjson = mapped_column(db.Text) - points = mapped_column(db.Text) - expl = mapped_column(db.Text) + question_id: Mapped[int] = mapped_column(primary_key=True) + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + par_id: Mapped[str] + question_title: Mapped[str] + answer: Mapped[Optional[str]] + questionjson: Mapped[Optional[str]] + points: Mapped[Optional[str]] + expl: Mapped[Optional[str]] diff --git a/timApp/lecture/questionactivity.py b/timApp/lecture/questionactivity.py index 6ace89dc90..8472e47bb5 100644 --- a/timApp/lecture/questionactivity.py +++ b/timApp/lecture/questionactivity.py @@ -1,9 +1,13 @@ from enum import Enum +from typing import TYPE_CHECKING -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +if TYPE_CHECKING: + from timApp.lecture.askedquestion import AskedQuestion + from timApp.user.user import User class QuestionActivityKind(Enum): Pointsclosed = 1 @@ -15,17 +19,13 @@ class QuestionActivityKind(Enum): class QuestionActivity(db.Model): __tablename__ = "question_activity" - - asked_id = mapped_column( - db.Integer, db.ForeignKey("askedquestion.asked_id"), primary_key=True + asked_id: Mapped[int] = mapped_column(db.ForeignKey("askedquestion.asked_id"), primary_key=True) + user_id: Mapped[int] = mapped_column( + db.ForeignKey("useraccount.id"), primary_key=True ) - user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), primary_key=True - ) - kind = mapped_column(db.Enum(QuestionActivityKind), primary_key=True) + kind: Mapped[QuestionActivityKind] = mapped_column(primary_key=True) - asked_question = db.relationship( - "AskedQuestion", back_populates="questionactivity", lazy="select" + asked_question: Mapped["AskedQuestion"] = db.relationship(back_populates="questionactivity", lazy="select" ) - user = db.relationship("User", back_populates="questionactivity", lazy="select") + user: Mapped["User"] = db.relationship(back_populates="questionactivity", lazy="select") diff --git a/timApp/lecture/runningquestion.py b/timApp/lecture/runningquestion.py index b60c247b06..a5732b0b81 100644 --- a/timApp/lecture/runningquestion.py +++ b/timApp/lecture/runningquestion.py @@ -1,27 +1,29 @@ from datetime import datetime +from typing import Optional, TYPE_CHECKING -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz + +if TYPE_CHECKING: + from timApp.lecture.askedquestion import AskedQuestion + from timApp.lecture.lecture import Lecture class Runningquestion(db.Model): - - - asked_id = mapped_column( - db.Integer, db.ForeignKey("askedquestion.asked_id"), primary_key=True + asked_id: Mapped[int] = mapped_column( + db.ForeignKey("askedquestion.asked_id"), primary_key=True ) - lecture_id = mapped_column( - db.Integer, db.ForeignKey("lecture.lecture_id"), primary_key=True + lecture_id: Mapped[int] = mapped_column( + db.ForeignKey("lecture.lecture_id"), primary_key=True ) # TODO should not be part of primary key (asked_id is enough) - ask_time = mapped_column( - db.DateTime(timezone=True), nullable=False, default=datetime.utcnow - ) - end_time = mapped_column(db.DateTime(timezone=True)) + ask_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) + end_time: Mapped[Optional[datetime_tz]] - asked_question = db.relationship( - "AskedQuestion", back_populates="running_question", lazy="select" - ) # : AskedQuestion - lecture = db.relationship( - "Lecture", back_populates="running_questions", lazy="select" + asked_question: Mapped["AskedQuestion"] = db.relationship( + back_populates="running_question", lazy="select" + ) + lecture: Mapped["Lecture"] = db.relationship( + back_populates="running_questions", lazy="select" ) diff --git a/timApp/lecture/showpoints.py b/timApp/lecture/showpoints.py index ddc6c92180..c89b703ba5 100644 --- a/timApp/lecture/showpoints.py +++ b/timApp/lecture/showpoints.py @@ -1,16 +1,18 @@ -from sqlalchemy.orm import mapped_column +from typing import TYPE_CHECKING + +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +if TYPE_CHECKING: + from timApp.lecture.askedquestion import AskedQuestion class Showpoints(db.Model): __tablename__ = "showpoints" - asked_id = mapped_column( - db.Integer, db.ForeignKey("askedquestion.asked_id"), primary_key=True + asked_id: Mapped[int] = mapped_column(db.ForeignKey("askedquestion.asked_id"), primary_key=True ) - asked_question = db.relationship( - "AskedQuestion", back_populates="showpoints", lazy="select" + asked_question: Mapped["AskedQuestion"] = db.relationship(back_populates="showpoints", lazy="select" ) diff --git a/timApp/lecture/useractivity.py b/timApp/lecture/useractivity.py index 72fea2da2e..f4c62072ab 100644 --- a/timApp/lecture/useractivity.py +++ b/timApp/lecture/useractivity.py @@ -1,22 +1,27 @@ +from typing import TYPE_CHECKING + from sqlalchemy import func -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz +if TYPE_CHECKING: + from timApp.user.user import User + from timApp.lecture.lecture import Lecture class Useractivity(db.Model): __tablename__ = "useractivity" - lecture_id = mapped_column( - db.Integer, db.ForeignKey("lecture.lecture_id"), primary_key=True + lecture_id: Mapped[int] = mapped_column( + db.ForeignKey("lecture.lecture_id"), primary_key=True ) - user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + user_id: Mapped[int] = mapped_column( + db.ForeignKey("useraccount.id"), primary_key=True ) - active = mapped_column( - db.DateTime(timezone=True), nullable=False, default=func.now() + active: Mapped[datetime_tz] = mapped_column(default=func.now() ) - user = db.relationship("User", back_populates="useractivity", lazy="select") - lecture = db.relationship("Lecture", back_populates="useractivity", lazy="select") + user: Mapped["User"] = db.relationship(back_populates="useractivity", lazy="select") + lecture: Mapped["Lecture"] = db.relationship(back_populates="useractivity", lazy="select") diff --git a/timApp/messaging/messagelist/messagelist_models.py b/timApp/messaging/messagelist/messagelist_models.py index 5bab9e5550..cd46c991be 100644 --- a/timApp/messaging/messagelist/messagelist_models.py +++ b/timApp/messaging/messagelist/messagelist_models.py @@ -1,10 +1,10 @@ from datetime import datetime from enum import Enum -from typing import Optional, Any +from typing import Optional, Any, TYPE_CHECKING, List from sqlalchemy import select from sqlalchemy.ext.hybrid import hybrid_property # type: ignore -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound # type: ignore from timApp.messaging.messagelist.listinfo import ( @@ -16,8 +16,13 @@ MessageVerificationType, ) from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.util.utils import get_current_time +if TYPE_CHECKING: + from timApp.item.block import Block + from timApp.user.usergroup import UserGroup + class MemberJoinMethod(Enum): """How a user was added to a message list.""" @@ -35,83 +40,80 @@ class MessageListModel(db.Model): """Database model for message lists""" __tablename__ = "messagelist" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) - manage_doc_id = mapped_column(db.Integer, db.ForeignKey("block.id")) + manage_doc_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) """The document which manages a message list.""" - name = mapped_column(db.Text) + name: Mapped[str] """The name of a message list.""" - can_unsubscribe = mapped_column(db.Boolean) + can_unsubscribe: Mapped[bool] """If a member can unsubscribe from this list on their own.""" - email_list_domain = mapped_column(db.Text) + email_list_domain: Mapped[str] """The domain used for an email list attached to a message list. If None/null, then message list doesn't have an attached email list. This is a tad silly at this point in time, because JYU TIM only has one domain. However, this allows quick adaptation if more domains are added or otherwise changed in the future. """ - archive = mapped_column(db.Enum(ArchiveType)) + archive: Mapped[ArchiveType] """The archive policy of a message list.""" - notify_owner_on_change = mapped_column(db.Boolean) + notify_owner_on_change: Mapped[bool] """Should the owner of the message list be notified if there are changes on message list members.""" - description = mapped_column(db.Text) + description: Mapped[str] """A short description what a message list is about.""" - info = mapped_column(db.Text) + info: Mapped[str] """Additional information about the message list.""" - removed = mapped_column(db.DateTime(timezone=True)) + removed: Mapped[datetime_tz] """When this list has been marked for removal.""" - default_send_right = mapped_column(db.Boolean) + default_send_right: Mapped[bool] """Default send right for new members who join the list on their own.""" - default_delivery_right = mapped_column(db.Boolean) + default_delivery_right: Mapped[bool] """Default delivery right for new members who join the list on their own.""" - tim_user_can_join = mapped_column(db.Boolean) + tim_user_can_join: Mapped[bool] """Flag if TIM users can join the list on their own.""" - subject_prefix = mapped_column(db.Text) + subject_prefix: Mapped[str] """What prefix message subjects that go through the list get.""" - only_text = mapped_column(db.Boolean) + only_text: Mapped[bool] """Flag if only text format messages are allowed on a list.""" - default_reply_type = mapped_column(db.Enum(ReplyToListChanges)) + default_reply_type: Mapped[ReplyToListChanges] """Default reply type for the list.""" - non_member_message_pass = mapped_column(db.Boolean) + non_member_message_pass: Mapped[bool] """Flag if non members messages to the list are passed straight through without moderation.""" - allow_attachments = mapped_column(db.Boolean) + allow_attachments: Mapped[bool] """Flag if attachments are allowed on the list. The list of allowed attachment file extensions are stored at listoptions.py """ - message_verification = mapped_column( - db.Enum(MessageVerificationType), - nullable=False, + message_verification: Mapped[MessageVerificationType] = mapped_column( default=MessageVerificationType.MUNGE_FROM, ) """How to verify messages sent to the list.""" - block = db.relationship( - "Block", back_populates="managed_messagelist", lazy="select" + block: Mapped["Block"] = db.relationship( + back_populates="managed_messagelist", lazy="select" ) """Relationship to the document that is used to manage this message list.""" - members = db.relationship( - "MessageListMember", back_populates="message_list", lazy="select" - ) # : list["MessageListTimMember"] + members: Mapped[List["MessageListMember"]] = db.relationship( + back_populates="message_list", lazy="select" + ) """All the members of the list.""" - distribution = db.relationship( - "MessageListDistribution", back_populates="message_list", lazy="select" + distribution: Mapped["MessageListDistribution"] = db.relationship( + back_populates="message_list", lazy="select" ) """The message channels the list uses.""" @@ -267,56 +269,55 @@ class MessageListMember(db.Model): """Database model for members of a message list.""" __tablename__ = "messagelist_member" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) - message_list_id = mapped_column(db.Integer, db.ForeignKey("messagelist.id")) + message_list_id: Mapped[int] = mapped_column(db.ForeignKey("messagelist.id")) """What message list a member belongs to.""" - send_right = mapped_column(db.Boolean) + send_right: Mapped[bool] """If a member can send messages to a message list.""" - delivery_right = mapped_column(db.Boolean) + delivery_right: Mapped[bool] """If a member can get messages from a message list.""" - membership_ended = mapped_column(db.DateTime(timezone=True)) + membership_ended: Mapped[Optional[datetime_tz]] """When member's membership on a list ended. This is set when member is removed from a list. A value of None means the member is still on the list.""" - join_method = mapped_column(db.Enum(MemberJoinMethod)) + join_method: Mapped[MemberJoinMethod] """How the member came to a list.""" - membership_verified = mapped_column(db.DateTime(timezone=True)) + membership_verified: Mapped[Optional[datetime_tz]] """When the user's joining was verified. If user is added e.g. by a teacher to a course's message list, this date is the date teacher added the member. If the member was invited, then this is the date they verified their join. """ - member_type = mapped_column(db.Text) + member_type: Mapped[str] """Discriminator for polymorhphic members.""" - message_list = db.relationship( - "MessageListModel", back_populates="members", lazy="select", uselist=False + message_list: Mapped["MessageListModel"] = db.relationship( + back_populates="members", lazy="select" ) - tim_member = db.relationship( - "MessageListTimMember", + tim_member: Mapped[Optional["MessageListTimMember"]] = db.relationship( back_populates="member", lazy="select", - uselist=False, post_update=True, ) - external_member = db.relationship( - "MessageListExternalMember", + external_member: Mapped[Optional["MessageListExternalMember"]] = db.relationship( back_populates="member", lazy="select", uselist=False, post_update=True, ) - distribution = db.relationship( - "MessageListDistribution", back_populates="member", lazy="select" + distribution: Mapped[Optional["MessageListDistribution"]] = db.relationship( + back_populates="member", lazy="select" ) - __mapper_args__ = {"polymorphic_identity": "member", "polymorphic_on": member_type} + __mapper_args__ = { + "polymorphic_identity": "member", + "polymorphic_on": "member_type", + } def is_external_member(self) -> bool: """If this member is an external member to a message list.""" @@ -403,24 +404,20 @@ class MessageListTimMember(MessageListMember): __tablename__ = "messagelist_tim_member" - id = mapped_column(db.Integer, db.ForeignKey("messagelist_member.id"), primary_key=True) + id: Mapped[int] = mapped_column( + db.ForeignKey("messagelist_member.id"), primary_key=True + ) - group_id = mapped_column(db.Integer, db.ForeignKey("usergroup.id")) + group_id: Mapped[int] = mapped_column(db.ForeignKey("usergroup.id")) """A UserGroup id for a member.""" - member = db.relationship( - "MessageListMember", + member: Mapped["MessageListMember"] = db.relationship( back_populates="tim_member", - lazy="select", - uselist=False, post_update=True, ) - user_group = db.relationship( - "UserGroup", + user_group: Mapped["UserGroup"] = db.relationship( back_populates="messagelist_membership", - lazy="select", - uselist=False, post_update=True, ) @@ -462,19 +459,18 @@ class MessageListExternalMember(MessageListMember): __tablename__ = "messagelist_external_member" - id = mapped_column(db.Integer, db.ForeignKey("messagelist_member.id"), primary_key=True) + id: Mapped[int] = mapped_column( + db.ForeignKey("messagelist_member.id"), primary_key=True + ) - email_address = mapped_column(db.Text) + email_address: Mapped[str] """Email address of message list's external member.""" - display_name = mapped_column(db.Text) + display_name: Mapped[str] """Display name for external user, which in most cases should be the external member's address' owner's name.""" - member = db.relationship( - "MessageListMember", - back_populates="external_member", - lazy="select", - uselist=False, + member: Mapped["MessageListMember"] = db.relationship( + back_populates="external_member" ) __mapper_args__ = {"polymorphic_identity": "external_member"} @@ -509,22 +505,25 @@ class MessageListDistribution(db.Model): """Message list member's chosen distribution channels.""" __tablename__ = "messagelist_distribution" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) - user_id = mapped_column(db.Integer, db.ForeignKey("messagelist_member.id")) + user_id: Mapped[Optional[int]] = mapped_column( + db.ForeignKey("messagelist_member.id") + ) """Message list member's id, if this row is about message list member's channel distribution.""" - message_list_id = mapped_column(db.Integer, db.ForeignKey("messagelist.id")) + message_list_id: Mapped[Optional[int]] = mapped_column( + db.ForeignKey("messagelist.id") + ) """Message list's id, if this row is about message list's channel distribution.""" - channel = mapped_column(db.Enum(Channel)) + channel: Mapped[Channel] """Which message channels are used by a message list or a user.""" - member = db.relationship( - "MessageListMember", back_populates="distribution", lazy="select", uselist=False + member: Mapped[Optional["MessageListMember"]] = db.relationship( + back_populates="distribution", lazy="select" ) - message_list = db.relationship( - "MessageListModel", back_populates="distribution", lazy="select", uselist=False + message_list: Mapped[Optional["MessageListModel"]] = db.relationship( + back_populates="distribution", lazy="select" ) diff --git a/timApp/messaging/timMessage/internalmessage_models.py b/timApp/messaging/timMessage/internalmessage_models.py index f65f6624e8..1bdd743dbb 100644 --- a/timApp/messaging/timMessage/internalmessage_models.py +++ b/timApp/messaging/timMessage/internalmessage_models.py @@ -1,13 +1,17 @@ +from datetime import datetime from enum import Enum -from typing import Any, Optional, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING, List from sqlalchemy import func, select -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.user.user import User + from timApp.item.block import Block + from timApp.user.usergroup import UserGroup class DisplayType(Enum): @@ -20,39 +24,38 @@ class InternalMessage(db.Model): __tablename__ = "internalmessage" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """Message identifier.""" - created = mapped_column(db.DateTime(timezone=True), nullable=False, default=func.now()) + created: Mapped[datetime_tz] = mapped_column(default=func.now()) """Date and time when the message was created.""" - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) """Block identifier.""" - par_id = mapped_column(db.Text, nullable=False) + par_id: Mapped[str] """Paragraph identifier.""" - can_mark_as_read = mapped_column(db.Boolean, nullable=False) + can_mark_as_read: Mapped[bool] """Whether the recipient can mark the message as read.""" - reply = mapped_column(db.Boolean, nullable=False) + reply: Mapped[bool] """Whether the message can be replied to.""" - display_type = mapped_column(db.Enum(DisplayType), nullable=False) + display_type: Mapped[DisplayType] """How the message is displayed.""" - expires = mapped_column(db.DateTime) + expires: Mapped[Optional[datetime]] = mapped_column(db.DateTime) """"When the message display will disappear.""" - replies_to = mapped_column(db.Integer) + replies_to: Mapped[Optional[int]] """Id of the message which this messages is a reply to""" - displays = db.relationship("InternalMessageDisplay", back_populates="message") - readreceipts = db.relationship( - "InternalMessageReadReceipt", back_populates="message" - ) # : list["InternalMessageReadReceipt"] - block = db.relationship("Block", back_populates="internalmessage") + displays: Mapped[List["InternalMessageDisplay"]] = db.relationship(back_populates="message") + readreceipts: Mapped[List["InternalMessageReadReceipt"]] = db.relationship( + back_populates="message" + ) + block: Mapped["Block"] = db.relationship(back_populates="internalmessage") def to_json(self) -> dict[str, Any]: return { @@ -74,26 +77,26 @@ class InternalMessageDisplay(db.Model): __tablename__ = "internalmessage_display" - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """Message display identifier.""" - message_id = mapped_column( - db.Integer, db.ForeignKey("internalmessage.id"), nullable=False + message_id: Mapped[int] = mapped_column( + db.ForeignKey("internalmessage.id") ) """Message identifier.""" - usergroup_id = mapped_column(db.Integer, db.ForeignKey("usergroup.id")) + usergroup_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("usergroup.id")) """Who sees the message; if null, displayed for everyone.""" - display_doc_id = mapped_column(db.Integer, db.ForeignKey("block.id")) + display_doc_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) """ Identifier for the document or the folder where the message is displayed. If null, the message is displayed globally. """ - message = db.relationship("InternalMessage", back_populates="displays") - usergroup = db.relationship("UserGroup", back_populates="internalmessage_display") - display_block = db.relationship("Block", back_populates="internalmessage_display") + message: Mapped["InternalMessage"] = db.relationship(back_populates="displays") + usergroup: Mapped[Optional["UserGroup"]] = db.relationship(back_populates="internalmessage_display") + display_block: Mapped[Optional["Block"]] = db.relationship(back_populates="internalmessage_display") def to_json(self) -> dict[str, Any]: return { @@ -110,22 +113,22 @@ class InternalMessageReadReceipt(db.Model): __tablename__ = "internalmessage_readreceipt" - message_id = mapped_column( - db.Integer, db.ForeignKey("internalmessage.id"), primary_key=True + message_id: Mapped[int] = mapped_column( + db.ForeignKey("internalmessage.id"), primary_key=True ) """Message identifier.""" - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), primary_key=True) + user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id"), primary_key=True) """Identifier for the user who marked the message as read.""" - last_seen = mapped_column(db.DateTime) + last_seen: Mapped[Optional[datetime]] """Timestamp for the last time the the message was displayed to the user""" - marked_as_read_on = mapped_column(db.DateTime) + marked_as_read_on: Mapped[Optional[datetime]] """Timestamp for when the message was marked as read.""" - message = db.relationship("InternalMessage", back_populates="readreceipts") - user = db.relationship("User", back_populates="internalmessage_readreceipt") + message: Mapped["InternalMessage"] = db.relationship(back_populates="readreceipts") + user: Mapped["User"] = db.relationship(back_populates="internalmessage_readreceipt") @staticmethod def get_for_user( diff --git a/timApp/note/usernote.py b/timApp/note/usernote.py index 9cca12121d..67ffea5700 100644 --- a/timApp/note/usernote.py +++ b/timApp/note/usernote.py @@ -1,54 +1,56 @@ +from typing import Optional, TYPE_CHECKING + from sqlalchemy import func -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz + +if TYPE_CHECKING: + from timApp.user.usergroup import UserGroup + from timApp.item.block import Block class UserNote(db.Model): """A comment/note that has been posted in a document paragraph.""" __tablename__ = "usernotes" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """Comment id.""" - usergroup_id = mapped_column( - db.Integer, db.ForeignKey("usergroup.id"), nullable=False - ) + usergroup_id: Mapped[int] = mapped_column(db.ForeignKey("usergroup.id")) """The UserGroup id who posted the comment.""" - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) """The document id in which this comment was posted.""" - par_id = mapped_column(db.Text, nullable=False) + par_id: Mapped[str] """The paragraph id in which this comment was posted.""" - par_hash = mapped_column(db.Text, nullable=False) + par_hash: Mapped[str] """The paragraph hash at the time this comment was posted.""" - content = mapped_column(db.Text, nullable=False) + content: Mapped[str] """Comment content.""" - created = mapped_column( - db.DateTime(timezone=True), nullable=False, default=func.now() - ) + created: Mapped[datetime_tz] = mapped_column(default=func.now()) """Comment creation timestamp.""" - modified = mapped_column(db.DateTime(timezone=True)) + modified: Mapped[Optional[datetime_tz]] """Comment modification timestamp.""" - access = mapped_column(db.Text, nullable=False) + access: Mapped[str] """Who can see this comment. So far valid values are 'everyone' and 'justme'.""" - tags = mapped_column(db.Text, nullable=False) + tags: Mapped[str] """Tags for the comment.""" - html = mapped_column(db.Text) + html: Mapped[Optional[str]] """Comment HTML cache.""" - usergroup = db.relationship("UserGroup", back_populates="notes") - block = db.relationship("Block") + usergroup: Mapped["UserGroup"] = relationship(back_populates="notes") + block: Mapped["Block"] = relationship() @property def is_public(self) -> bool: diff --git a/timApp/notification/notification.py b/timApp/notification/notification.py index ef655834f8..4a1d34d8ec 100644 --- a/timApp/notification/notification.py +++ b/timApp/notification/notification.py @@ -1,11 +1,16 @@ import enum +from typing import TYPE_CHECKING -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.item.block import BlockType +from timApp.item.block import BlockType, Block from timApp.timdb.sqa import db, is_attribute_loaded from timApp.util.logger import log_warning +if TYPE_CHECKING: + from timApp.user.user import User + from timApp.item.block import Block + class NotificationType(enum.Enum): DocModified = 1 @@ -31,21 +36,20 @@ class Notification(db.Model): """Notification settings for a User for a block.""" __tablename__ = "notification" - - user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + user_id: Mapped[int] = mapped_column( + db.ForeignKey("useraccount.id"), primary_key=True ) """User id.""" - block_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + block_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) """Item id.""" - notification_type = mapped_column(db.Enum(NotificationType), primary_key=True) + notification_type: Mapped[NotificationType] = mapped_column(primary_key=True) """Notification type.""" - user = db.relationship("User", back_populates="notifications") - block = db.relationship("Block", back_populates="notifications") # : Block + user: Mapped["User"] = relationship(back_populates="notifications") + block: Mapped["Block"] = relationship(back_populates="notifications") def to_json(self) -> dict: j = {"type": self.notification_type} diff --git a/timApp/notification/pending_notification.py b/timApp/notification/pending_notification.py index 6e9e2269fb..8f05c3ad0b 100644 --- a/timApp/notification/pending_notification.py +++ b/timApp/notification/pending_notification.py @@ -1,29 +1,35 @@ +from typing import Optional, TYPE_CHECKING + from sqlalchemy import func, select -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.document.version import Version from timApp.notification.notification import NotificationType from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz + +if TYPE_CHECKING: + from timApp.user.user import User + from timApp.item.block import Block GroupingKey = tuple[int, str] class PendingNotification(db.Model): __tablename__ = "pendingnotification" - - id = mapped_column(db.Integer, primary_key=True) - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) - discriminant = mapped_column(db.Text, nullable=False) - par_id = mapped_column(db.Text, nullable=True) - text = mapped_column(db.Text, nullable=True) - created = mapped_column(db.DateTime(timezone=True), nullable=False, default=func.now()) - processed = mapped_column(db.DateTime(timezone=True), nullable=True, index=True) - kind = mapped_column(db.Enum(NotificationType), nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + discriminant: Mapped[str] + par_id: Mapped[Optional[str]] + text: Mapped[Optional[str]] + created: Mapped[datetime_tz] = mapped_column(default=func.now()) + processed: Mapped[Optional[datetime_tz]] = mapped_column(index=True) + kind: Mapped[NotificationType] - user = db.relationship("User", lazy="selectin") # : User - block = db.relationship("Block") + user: Mapped["User"] = relationship(lazy="selectin") + block: Mapped["Block"] = relationship() @property def grouping_key(self) -> GroupingKey: @@ -33,13 +39,13 @@ def grouping_key(self) -> GroupingKey: def notify_type(self) -> NotificationType: return self.kind - __mapper_args__ = {"polymorphic_on": discriminant} + __mapper_args__ = {"polymorphic_on": "discriminant"} class DocumentNotification(PendingNotification): """A notification that a document has changed.""" - version_change = mapped_column(db.Text) # : str # like "1,2/1,3" + version_change = mapped_column(db.Text) # : str # like "1,2/1,3" @property def version_before(self) -> Version: diff --git a/timApp/peerreview/peerreview.py b/timApp/peerreview/peerreview.py index 5cdae42c0d..9299f238a0 100644 --- a/timApp/peerreview/peerreview.py +++ b/timApp/peerreview/peerreview.py @@ -1,58 +1,60 @@ -from typing import Any +from typing import Any, Optional, TYPE_CHECKING -from sqlalchemy.orm import mapped_column +from sqlalchemy import UniqueConstraint +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz + +if TYPE_CHECKING: + from timApp.user.user import User class PeerReview(db.Model): """A peer review to a task.""" __tablename__ = "peer_review" - - - id = mapped_column(db.Integer, primary_key=True) + + id: Mapped[int] = mapped_column(primary_key=True) """Review identifier.""" - answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id"), nullable=True) + answer_id: Mapped[int] = mapped_column(db.ForeignKey("answer.id")) """Answer id.""" - task_name = mapped_column(db.Text, nullable=True) + task_name: Mapped[Optional[str]] """Task name""" - block_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) + block_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) """Doc id""" - reviewer_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + reviewer_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) """Reviewer id""" - reviewable_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), nullable=False - ) + reviewable_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) """Reviewable id""" - start_time = mapped_column(db.DateTime(timezone=True), nullable=False) + start_time: Mapped[datetime_tz] """Review start time""" - end_time = mapped_column(db.DateTime(timezone=True), nullable=False) + end_time: Mapped[datetime_tz] """Review end time""" - reviewed = mapped_column(db.Boolean, default=False) + reviewed: Mapped[bool] = mapped_column(default=False) """Review status""" - points = mapped_column(db.Float) + points: Mapped[Optional[float]] """Points given by the reviewer""" - comment = mapped_column(db.Text) + comment: Mapped[Optional[str]] """Review comment""" __table_args__ = ( - db.UniqueConstraint("answer_id", "block_id", "reviewer_id", "reviewable_id"), - db.UniqueConstraint("task_name", "block_id", "reviewer_id", "reviewable_id"), + UniqueConstraint("answer_id", "block_id", "reviewer_id", "reviewable_id"), + UniqueConstraint("task_name", "block_id", "reviewer_id", "reviewable_id"), ) - reviewer = db.relationship("User", foreign_keys=[reviewer_id]) - reviewable = db.relationship("User", foreign_keys=[reviewable_id]) + reviewer: Mapped["User"] = relationship(foreign_keys=[reviewer_id]) + reviewable: Mapped["User"] = relationship(foreign_keys=[reviewable_id]) def to_json(self) -> dict[str, Any]: return { diff --git a/timApp/plugin/calendar/models.py b/timApp/plugin/calendar/models.py index cf1ed5abbc..3a751cb412 100644 --- a/timApp/plugin/calendar/models.py +++ b/timApp/plugin/calendar/models.py @@ -13,38 +13,43 @@ __date__ = "24.5.2022" from dataclasses import dataclass -from typing import Optional, Iterable +from typing import Optional, Iterable, List, TYPE_CHECKING from sqlalchemy import func, select -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.user.user import User from timApp.user.usergroup import UserGroup from tim_common.dumboclient import call_dumbo +if TYPE_CHECKING: + from timApp.item.block import Block + class EventGroup(db.Model): """Information about a user group participating in an event.""" __tablename__ = "eventgroup" - - event_id = mapped_column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) + event_id: Mapped[int] = mapped_column( + db.ForeignKey("event.event_id"), primary_key=True + ) """Event the the group belongs to""" - usergroup_id = mapped_column( - db.Integer, db.ForeignKey("usergroup.id"), primary_key=True + usergroup_id: Mapped[int] = mapped_column( + db.ForeignKey("usergroup.id"), primary_key=True ) """The usergroup that belongs to the group""" - manager = mapped_column(db.Boolean) + manager: Mapped[Optional[bool]] """Is the group a manager (i.e. is able to modify event settings)?""" - extra = mapped_column(db.Boolean, nullable=False, default=False) + extra: Mapped[bool] = mapped_column(default=False) """Is this group an extra group (i.e. can it enroll without affecting the event capacity)?""" - user_group = db.relationship(UserGroup, lazy="select") + user_group: Mapped["UserGroup"] = relationship(lazy="select") """The usergroup that belongs to the group""" @@ -52,31 +57,32 @@ class Enrollment(db.Model): """A single enrollment in an event""" __tablename__ = "enrollment" - - event_id = mapped_column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) + event_id: Mapped[int] = mapped_column( + db.ForeignKey("event.event_id"), primary_key=True + ) """Event the enrollment is for""" - usergroup_id = mapped_column( - db.Integer, db.ForeignKey("usergroup.id"), primary_key=True + usergroup_id: Mapped[int] = mapped_column( + db.ForeignKey("usergroup.id"), primary_key=True ) """The usergroup that is enrolled (i.e. booked) in the event""" - booker_message = mapped_column(db.Text) + booker_message: Mapped[Optional[str]] """The message left by the booker""" - enroll_type_id = mapped_column( - db.Integer, db.ForeignKey("enrollmenttype.enroll_type_id"), nullable=False + enroll_type_id: Mapped[int] = mapped_column( + db.ForeignKey("enrollmenttype.enroll_type_id") ) """Type of the enrollment""" - event = db.relationship("Event", lazy="select") + event: Mapped["Event"] = relationship() """The event the enrollment is related to""" - usergroup = db.relationship(UserGroup, lazy="select") + usergroup: Mapped["UserGroup"] = relationship() """User group that booked the event""" - extra = mapped_column(db.Boolean, nullable=False, default=False) + extra: Mapped[bool] = mapped_column(default=False) """Is this an extra enrollment (i.e. can it enroll without affecting the event capacity)?""" @staticmethod @@ -100,11 +106,14 @@ class EventTagAttachment(db.Model): """Attachment information for the event tag""" __tablename__ = "eventtagattachment" - - event_id = mapped_column(db.Integer, db.ForeignKey("event.event_id"), primary_key=True) + event_id: Mapped[int] = mapped_column( + db.ForeignKey("event.event_id"), primary_key=True + ) """Event the tag is attached to""" - tag_id = mapped_column(db.Integer, db.ForeignKey("eventtag.tag_id"), primary_key=True) + tag_id: Mapped[int] = mapped_column( + db.ForeignKey("eventtag.tag_id"), primary_key=True + ) """Tag that is attached to the event""" @@ -112,20 +121,17 @@ class EventTag(db.Model): """A string tag that can be attached to an event""" __tablename__ = "eventtag" - - tag_id = mapped_column(db.Integer, primary_key=True) + tag_id: Mapped[int] = mapped_column(primary_key=True) """The id of the tag""" - tag = mapped_column(db.Text, nullable=False) + tag: Mapped[str] """The tag itself""" - events = db.relationship( - "Event", + events: Mapped[List["Event"]] = relationship( secondary=EventTagAttachment.__table__, - lazy="select", back_populates="tags", - ) # : list["Event"] + ) @staticmethod def get_or_create(tags: Iterable[str]) -> list["EventTag"]: @@ -181,82 +187,72 @@ class Event(db.Model): """A calendar event. Event has metadata (title, time, location) and various participating user groups.""" __tablename__ = "event" - - event_id = mapped_column(db.Integer, primary_key=True) + event_id: Mapped[int] = mapped_column(primary_key=True) """Identification number of the event""" - location = mapped_column(db.Text) + location: Mapped[Optional[str]] """Location of the event""" - max_size = mapped_column(db.Integer) + max_size: Mapped[Optional[int]] """How many people can attend the event""" - start_time = mapped_column(db.DateTime(timezone=True), nullable=False) + start_time: Mapped[datetime_tz] """Start time of the event""" - end_time = mapped_column(db.DateTime(timezone=True), nullable=False) + end_time: Mapped[datetime_tz] """End time of the event""" - message = mapped_column(db.Text) + message: Mapped[Optional[str]] """Message visible to anyone who can see the event""" - title = mapped_column(db.Text, nullable=False) + title: Mapped[str] """Title of the event""" - signup_before = mapped_column(db.DateTime(timezone=True)) + signup_before: Mapped[Optional[datetime_tz]] """Time until signup is closed""" - creator_user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), nullable=False - ) + creator_user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) """User who created the event originally""" - origin_doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=True) + origin_doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) """Document that was used to create the event""" - origin_doc = db.relationship("Block", lazy="select") + origin_doc: Mapped["Block"] = relationship() """Document that was used to create the event""" - enrolled_users = db.relationship( - UserGroup, - Enrollment.__table__, + enrolled_users: Mapped[List["UserGroup"]] = relationship( + secondary=Enrollment.__table__, primaryjoin=event_id == Enrollment.event_id, - lazy="select", overlaps="event, usergroup", - ) # : list[UserGroup] + ) """List of usergroups that are enrolled in the event""" - enrollments = db.relationship( - Enrollment, - lazy="select", + enrollments: Mapped[List["Enrollment"]] = relationship( back_populates="event", cascade="all, delete-orphan", overlaps="enrolled_users", - ) # : list[Enrollment] + ) """Enrollment information for the event""" - creator = db.relationship(User) # : User + creator: Mapped["User"] = relationship() """User who created the event originally""" - send_notifications = mapped_column(db.Boolean, nullable=False, default=True) + send_notifications: Mapped[bool] = mapped_column(default=True) """Whether to send notifications related to enrollment to the event""" - important = mapped_column(db.Boolean, nullable=False, default=False) + important: Mapped[bool] = mapped_column(default=False) """Whether the event is important (i.e. should be show as special in calendar)""" - event_groups = db.relationship( - EventGroup, + event_groups: Mapped[List["EventGroup"]] = relationship( foreign_keys="EventGroup.event_id", cascade="all,delete-orphan", - ) # : list[EventGroup] + ) - tags = db.relationship( - EventTag, + tags: Mapped[List["EventTag"]] = relationship( secondary=EventTagAttachment.__table__, - lazy="select", back_populates="events", - ) # : list[EventTag] + ) """Tags attached to the event""" @property @@ -423,12 +419,11 @@ class EnrollmentType(db.Model): """Table for enrollment type, combines enrollment type ID to specific enrollment type""" __tablename__ = "enrollmenttype" - - enroll_type_id = mapped_column(db.Integer, primary_key=True) + enroll_type_id: Mapped[int] = mapped_column(primary_key=True) """Enrollment type""" - enroll_type = mapped_column(db.Text, nullable=False) + enroll_type: Mapped[str] """Name of the enrollment type""" @@ -436,14 +431,13 @@ class ExportedCalendar(db.Model): """Information about exported calendars""" __tablename__ = "exportedcalendar" - - - user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), primary_key=True, nullable=False + + user_id: Mapped[int] = mapped_column( + db.ForeignKey("useraccount.id"), primary_key=True ) """User who created the exported calendar""" - calendar_hash = mapped_column(db.Text, nullable=False) + calendar_hash: Mapped[str] """Hash of the exported calendar""" - user = db.relationship(User) + user: Mapped["User"] = relationship() diff --git a/timApp/plugin/plugintype.py b/timApp/plugin/plugintype.py index abecba7ac0..f5966715a5 100644 --- a/timApp/plugin/plugintype.py +++ b/timApp/plugin/plugintype.py @@ -4,7 +4,7 @@ import filelock from sqlalchemy import select from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped import timApp from timApp.timdb.sqa import db @@ -36,11 +36,10 @@ def to_json(self) -> dict[str, Any]: # TODO: Right now values are added dynamically to the table when saving answers. Instead add them on TIM start. class PluginType(db.Model, PluginTypeBase): __tablename__ = "plugintype" - - - id = mapped_column(db.Integer, primary_key=True) - type = mapped_column(db.Text, nullable=False, unique=True) + id: Mapped[int] = mapped_column(primary_key=True) + + type: Mapped[str] = mapped_column(unique=True) @staticmethod def resolve(p_type: str) -> "PluginType": diff --git a/timApp/plugin/timtable/row_owner_info.py b/timApp/plugin/timtable/row_owner_info.py index a72436f40e..ec0e159665 100644 --- a/timApp/plugin/timtable/row_owner_info.py +++ b/timApp/plugin/timtable/row_owner_info.py @@ -1,4 +1,6 @@ -from sqlalchemy.orm import mapped_column +from typing import Optional + +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db @@ -10,17 +12,14 @@ class RowOwnerInfo(db.Model): """ __tablename__ = "rowownerinfo" - - doc_id = mapped_column(db.Integer, primary_key=True) - par_id = mapped_column(db.Text, primary_key=True) - unique_row_id = mapped_column(db.Integer, primary_key=True) - usergroup_id = mapped_column( - db.Integer, db.ForeignKey("usergroup.id"), primary_key=False - ) + doc_id: Mapped[int] = mapped_column(primary_key=True) + par_id: Mapped[str] = mapped_column(primary_key=True) + unique_row_id: Mapped[int] = mapped_column(primary_key=True) + usergroup_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("usergroup.id")) - # usergroup = db.relationship('UserGroup', back_populates='rowOwnerInfo') - # block = db.relationship('Block', back_populates='tags') + # usergroup = relationship('UserGroup', back_populates='rowOwnerInfo') + # block = relationship('Block', back_populates='tags') def __json__(self): return ["doc_id", "par_id", "unique_row_id", "usergroup_id"] diff --git a/timApp/printing/printeddoc.py b/timApp/printing/printeddoc.py index 88c3905e98..9d257bf7fa 100644 --- a/timApp/printing/printeddoc.py +++ b/timApp/printing/printeddoc.py @@ -1,7 +1,10 @@ +from typing import Optional + from sqlalchemy import func -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz class PrintedDoc(db.Model): @@ -9,32 +12,27 @@ class PrintedDoc(db.Model): (CSS printing does not count because it happens entirely in browser).""" __tablename__ = "printed_doc" - - id = mapped_column(db.Integer, primary_key=True) - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) """Id of the printed document.""" - template_doc_id = mapped_column( - db.Integer, db.ForeignKey("block.id"), nullable=True - ) + template_doc_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) """Id of the template document.""" - file_type = mapped_column(db.Text, nullable=False) + file_type: Mapped[str] """The filetype of the print.""" - path_to_file = mapped_column(db.Text, nullable=True) + path_to_file: Mapped[str] """Path to the printed document in the filesystem.""" - version = mapped_column(db.Text, nullable=False) + version: Mapped[str] """Version (in practice, a hash) for identifying whether a document has already been printed and can be fetched from cache. """ - temp = mapped_column(db.Boolean, default=True, nullable=False) + temp: Mapped[bool] = mapped_column(default=True) """Whether the printed document is stored only temporarily (gets deleted after some time).""" - created = mapped_column( - db.DateTime(timezone=True), default=func.now(), nullable=False - ) + created: Mapped[datetime_tz] = mapped_column(default=func.now()) """Timestamp of printing.""" diff --git a/timApp/proxy/routes.py b/timApp/proxy/routes.py index 730c0fab4a..8a558b3f9a 100644 --- a/timApp/proxy/routes.py +++ b/timApp/proxy/routes.py @@ -47,6 +47,7 @@ def getproxy( mimetype=mimetype, ) add_csp_header(resp, "sandbox allow-scripts") + resp.headers["Access-Control-Allow-Origin"] = "*" return resp if file and r.status_code == 200: filename = basename(parsed.path) or "download" diff --git a/timApp/readmark/readparagraph.py b/timApp/readmark/readparagraph.py index df97a4b6ce..cf0531facb 100644 --- a/timApp/readmark/readparagraph.py +++ b/timApp/readmark/readparagraph.py @@ -1,39 +1,40 @@ +from typing import TYPE_CHECKING + from sqlalchemy import func -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.readmark.readparagraphtype import ReadParagraphType from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz + +if TYPE_CHECKING: + from timApp.user.usergroup import UserGroup class ReadParagraph(db.Model): """Denotes that a User(Group) has read a specific paragraph in some way.""" __tablename__ = "readparagraph" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """Readmark id.""" - usergroup_id = mapped_column( - db.Integer, db.ForeignKey("usergroup.id"), nullable=False - ) + usergroup_id: Mapped[int] = mapped_column(db.ForeignKey("usergroup.id")) """UserGroup id.""" - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id")) + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) """Document id.""" - par_id = mapped_column(db.Text, nullable=False) + par_id: Mapped[str] """Paragraph id.""" - type = mapped_column(db.Enum(ReadParagraphType), nullable=False) + type: Mapped[ReadParagraphType] """Readmark type.""" - par_hash = mapped_column(db.Text, nullable=False) + par_hash: Mapped[str] """Paragraph hash at the time the readmark was registered.""" - timestamp = mapped_column( - db.DateTime(timezone=True), nullable=False, default=func.now() - ) + timestamp: Mapped[datetime_tz] = mapped_column(default=func.now()) """The time the readmark was registered.""" __table_args__ = ( @@ -41,4 +42,4 @@ class ReadParagraph(db.Model): db.Index("readparagraph_doc_id_usergroup_id_idx", "doc_id", "usergroup_id"), ) - usergroup = db.relationship("UserGroup", back_populates="readparagraphs") + usergroup: Mapped["UserGroup"] = relationship(back_populates="readparagraphs") diff --git a/timApp/sisu/scimusergroup.py b/timApp/sisu/scimusergroup.py index 5ac65e3538..85094ca6c5 100644 --- a/timApp/sisu/scimusergroup.py +++ b/timApp/sisu/scimusergroup.py @@ -1,6 +1,6 @@ import re -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db @@ -12,10 +12,11 @@ class ScimUserGroup(db.Model): __tablename__ = "scimusergroup" - - - group_id = mapped_column(db.Integer, db.ForeignKey("usergroup.id"), primary_key=True) - external_id = mapped_column(db.Text, unique=True, nullable=False) + + group_id: Mapped[int] = mapped_column( + db.ForeignKey("usergroup.id"), primary_key=True + ) + external_id: Mapped[str] = mapped_column(unique=True) @property def is_studysubgroup(self) -> bool: diff --git a/timApp/slide/slidestatus.py b/timApp/slide/slidestatus.py index 7793f6867c..789b48a61c 100644 --- a/timApp/slide/slidestatus.py +++ b/timApp/slide/slidestatus.py @@ -1,14 +1,13 @@ -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db class SlideStatus(db.Model): __tablename__ = "slide_status" - - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - status = mapped_column(db.Text, nullable=False) + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + status: Mapped[str] def __init__(self, doc_id, status): self.doc_id = doc_id diff --git a/timApp/tim.py b/timApp/tim.py index 4d40682f18..266948948c 100755 --- a/timApp/tim.py +++ b/timApp/tim.py @@ -354,7 +354,7 @@ def log_request(response): status_code = response.status_code log_info(get_request_message(status_code)) if request.method in ("PUT", "POST", "DELETE"): - log_debug(request.get_json(silent=True)) + log_debug(str(request.get_json(silent=True))) return response diff --git a/timApp/timdb/sqa.py b/timApp/timdb/sqa.py index 3c10c5654a..07afdf89ea 100644 --- a/timApp/timdb/sqa.py +++ b/timApp/timdb/sqa.py @@ -7,11 +7,14 @@ """ import os +from typing import Optional from flask_sqlalchemy import SQLAlchemy from sqlalchemy import func, text from sqlalchemy.orm import mapped_column -from sqlalchemy.orm.base import instance_state +from sqlalchemy.orm.base import instance_state, Mapped + +from timApp.timdb.types import add_tim_types, datetime_tz session_options = { "future": True, @@ -25,12 +28,21 @@ session_options["expire_on_commit"] = False db = SQLAlchemy(session_options=session_options, engine_options=engine_options) +add_tim_types(db) + +# TODO: Finish up adding Mapping annotations to everywhere +# TODO: Replace db.Model with custom DeclarativeBase class that also specifies __tablename__ and custom types. +# See https://docs.sqlalchemy.org/en/20/orm/declarative_mixins.html +# TODO: Switch models to use dataclasses instead +# See https://docs.sqlalchemy.org/en/20/orm/dataclasses.html#declarative-dataclass-mapping +# This should fix DeeplTranslationService's extra args, see https://docs.sqlalchemy.org/en/20/orm/dataclasses.html#using-non-mapped-dataclass-fields class TimeStampMixin: - created = mapped_column(db.DateTime(timezone=True), nullable=True, default=func.now()) - modified = mapped_column( - db.DateTime(timezone=True), + created: Mapped[Optional[datetime_tz]] = mapped_column( + nullable=True, default=func.now() + ) + modified: Mapped[Optional[datetime_tz]] = mapped_column( nullable=True, default=func.now(), onupdate=func.now(), @@ -38,7 +50,9 @@ class TimeStampMixin: def tim_main_execute(sql: str, params=None): - return db.session.execute(text(sql), params, bind_arguments={"bind": get_tim_main_engine()}) + return db.session.execute( + text(sql), params, bind_arguments={"bind": get_tim_main_engine()} + ) def get_tim_main_engine(): diff --git a/timApp/timdb/timdb.py b/timApp/timdb/timdb.py deleted file mode 100644 index 2231a42c74..0000000000 --- a/timApp/timdb/timdb.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Defines the TimDb database class.""" - -num = 0 - -# Always 0 for now. -worker_pid = 0 - -DB_PART_NAMES = { - "notes", - "readings", - "users", - "images", - "uploads", - "files", - "documents", - "answers", - "questions", - "messages", - "lectures", - "folders", - "lecture_answers", - "velps", - "velp_groups", - "annotations", - "session", -} - - -# class TimDb: -# """DEPRECATED CLASS, DO NOT ADD NEW CODE! -# -# Handles saving and retrieving information from TIM database. -# """ -# -# instances = 0 -# -# def __init__( -# self, -# files_root_path: Path, -# current_user_name: str = "Anonymous", -# route_path: str = "", -# ): -# """Initializes TimDB with the specified files root path, SQLAlchemy session and user name. -# -# -# :param current_user_name: The username of the current user. -# :param files_root_path: The root path where all the files will be stored. -# :param route_path: Path for the route requesting the db -# -# """ -# self.files_root_path = files_root_path -# self.route_path = route_path -# self.current_user_name = current_user_name -# -# self.blocks_path = self.files_root_path / "blocks" -# for path in [self.blocks_path]: -# if not path.exists(): -# log_info(f"Creating directory: {path}") -# path.mkdir(parents=True, exist_ok=False) -# self.reset_attrs() -# -# def reset_attrs(self): -# self.num = 0 -# self.time = 0 -# self.engine = None -# self.db = None -# self.velps = None -# self.velp_groups = None -# -# def __getattribute__(self, item): -# """Used to open TimDb connection lazily.""" -# if item in DB_PART_NAMES and self.db is None: -# self.open() -# return object.__getattribute__(self, item) -# -# def open(self): -# global num -# num += 1 -# self.num = num -# self.time = time.time() -# log_debug( -# f"GetDb {worker_pid:2d} {self.num:6d} {'':2s} {'':3s} {'':7s} {self.route_path:s}" -# ) -# # log_info('TimDb-dstr {:2d} {:6d} {:2d} {:3d} {:7.5f} {:s}'.format(worker_pid,self.num, TimDb.instances, bes, time.time() - self.time, self.route_path)) -# waiting = False -# from timApp.tim_app import app -# -# while True: -# try: -# self.engine = db.get_engine(app) -# self.db = self.engine.connect().connection -# self.session = db.session -# break -# except Exception as err: -# if not waiting: -# log_warning("WaitDb " + str(self.num) + " " + str(err)) -# waiting = True -# sleep(0.1) -# -# if waiting: -# log_warning("ReadyDb " + str(self.num)) -# -# TimDb.instances += 1 -# # num_connections = self.get_pg_connections() -# # log_info('TimDb instances/PG connections: {}/{} (constructor)'.format(TimDb.instances, num_connections)) -# -# def get_pg_connections(self): -# """Returns the number of clients currently connected to PostgreSQL.""" -# cursor = self.db.cursor() -# cursor.execute("SELECT sum(numbackends) FROM pg_stat_database") -# num_connections = cursor.fetchone()[0] -# return num_connections -# -# def commit(self): -# """Commits any changes to the database.""" -# db.session.commit() -# if self.db: -# self.db.commit() -# -# def close(self) -> None: -# """Closes the database connection.""" -# if hasattr(self, "db") and self.db is not None: -# bes = -1 -# TimDb.instances -= 1 -# try: -# # bes = self.get_pg_connections() -# self.db.close() -# except Exception as err: -# log_error("close error: " + str(self.num) + " " + str(err)) -# -# log_debug( -# f"TimDb-dstr {worker_pid:2d} {self.num:6d} {TimDb.instances:2d} {bes:3d} {time.time() - self.time:7.5f} {self.route_path:s}" -# ) -# self.reset_attrs() diff --git a/timApp/timdb/types.py b/timApp/timdb/types.py new file mode 100644 index 0000000000..c0d69f3883 --- /dev/null +++ b/timApp/timdb/types.py @@ -0,0 +1,16 @@ +from datetime import datetime + +from flask_sqlalchemy import SQLAlchemy +from typing_extensions import Annotated + +datetime_tz = Annotated[datetime, "datetime_tz"] + + +def add_tim_types(db: SQLAlchemy) -> None: + # In TIM, we always use TEXT by default for strings + db.Model.registry.update_type_annotation_map( + { + str: db.Text, + datetime_tz: db.DateTime(timezone=True), + } + ) diff --git a/timApp/user/consentchange.py b/timApp/user/consentchange.py index 6408ddbffb..fd4917d72e 100644 --- a/timApp/user/consentchange.py +++ b/timApp/user/consentchange.py @@ -1,17 +1,17 @@ from sqlalchemy import func -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db -from timApp.user.user import Consent +from timApp.timdb.types import datetime_tz +from timApp.user.user import Consent, User class ConsentChange(db.Model): __tablename__ = "consentchange" - - id = mapped_column(db.Integer, primary_key=True) - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) - time = mapped_column(db.DateTime(timezone=True), nullable=False, default=func.now()) - consent = mapped_column(db.Enum(Consent), nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + time: Mapped[datetime_tz] = mapped_column(default=func.now()) + consent: Mapped[Consent] - user = db.relationship("User", back_populates="consents", lazy="select") + user: Mapped["User"] = relationship(back_populates="consents") diff --git a/timApp/user/hakaorganization.py b/timApp/user/hakaorganization.py index 053a22d9da..1ccd2771f9 100644 --- a/timApp/user/hakaorganization.py +++ b/timApp/user/hakaorganization.py @@ -1,19 +1,23 @@ from functools import lru_cache +from typing import TYPE_CHECKING from flask import current_app from sqlalchemy import select -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db +if TYPE_CHECKING: + from timApp.user.personaluniquecode import PersonalUniqueCode + class HakaOrganization(db.Model): - - - id = mapped_column(db.Integer, primary_key=True) - name = mapped_column(db.Text, nullable=False, unique=True) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(unique=True) - uniquecodes = db.relationship("PersonalUniqueCode", back_populates="organization") + uniquecodes: Mapped["PersonalUniqueCode"] = relationship( + back_populates="organization" + ) @staticmethod def get_or_create(name: str): diff --git a/timApp/user/newuser.py b/timApp/user/newuser.py index 76acd736dd..3d8f3ac4d4 100644 --- a/timApp/user/newuser.py +++ b/timApp/user/newuser.py @@ -1,7 +1,8 @@ from sqlalchemy import func -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.user.userutils import check_password_hash @@ -9,17 +10,14 @@ class NewUser(db.Model): """A user that is going to register to TIM via email and has not yet completed the registration process.""" __tablename__ = "newuser" - - email = mapped_column(db.Text, primary_key=True) + email: Mapped[str] = mapped_column(primary_key=True) """Email address.""" - pass_ = mapped_column("pass", db.Text, nullable=False, primary_key=True) + pass_: Mapped[str] = mapped_column("pass", primary_key=True) """Password hash for the temporary password.""" - created = mapped_column( - db.DateTime(timezone=True), nullable=False, default=func.now() - ) + created: Mapped[datetime_tz] = mapped_column(default=func.now()) """The time when user clicked "Sign up".""" def check_password(self, password: str) -> bool: diff --git a/timApp/user/personaluniquecode.py b/timApp/user/personaluniquecode.py index 9367324015..d7910127c2 100644 --- a/timApp/user/personaluniquecode.py +++ b/timApp/user/personaluniquecode.py @@ -1,44 +1,42 @@ import re from dataclasses import dataclass -from typing import Optional +from typing import Optional, TYPE_CHECKING -from sqlalchemy import select -from sqlalchemy.orm import mapped_column +from sqlalchemy import select, UniqueConstraint +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db from timApp.user.hakaorganization import HakaOrganization +if TYPE_CHECKING: + from timApp.user.user import User + class PersonalUniqueCode(db.Model): """The database model for the 'schacPersonalUniqueCode' Haka attribute.""" - - - user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), nullable=False, primary_key=True + user_id: Mapped[int] = mapped_column( + db.ForeignKey("useraccount.id"), primary_key=True ) """User id.""" - org_id = mapped_column( - db.Integer, - db.ForeignKey("haka_organization.id"), - nullable=False, - primary_key=True, + org_id: Mapped[int] = mapped_column( + db.ForeignKey("haka_organization.id"), primary_key=True ) """Organization id.""" - code = mapped_column(db.Text, nullable=False, index=True) - """The actual code. This could be e.g. student id or employee id.""" - - type = mapped_column(db.Text, nullable=False, primary_key=True) + type: Mapped[str] = mapped_column(primary_key=True) """The type of the code, e.g. student or employee.""" - user = db.relationship("User", back_populates="uniquecodes", lazy="selectin") - organization = db.relationship( - "HakaOrganization", back_populates="uniquecodes", lazy="selectin" + code: Mapped[str] = mapped_column(index=True) + """The actual code. This could be e.g. student id or employee id.""" + + user: Mapped["User"] = relationship(back_populates="uniquecodes", lazy="selectin") + organization: Mapped["HakaOrganization"] = relationship( + back_populates="uniquecodes", lazy="selectin" ) - __table_args__ = (db.UniqueConstraint("org_id", "code", "type"),) + __table_args__ = (UniqueConstraint("org_id", "code", "type"),) @property def user_collection_key(self): diff --git a/timApp/user/user.py b/timApp/user/user.py index 6b753829a1..9514c7b93d 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -3,17 +3,22 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Optional, Union +from typing import Optional, Union, Dict, Tuple, TYPE_CHECKING, List import filelock from flask import current_app, has_request_context from sqlalchemy import func, select, delete from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import ( + mapped_column, + Mapped, + attribute_keyed_dict, + relationship, + DynamicMapped, +) from sqlalchemy.orm import ( selectinload, defaultload, - attribute_mapped_collection, ) from sqlalchemy.orm.interfaces import LoaderOption from sqlalchemy.sql import Select @@ -78,6 +83,20 @@ ) from tim_common.timjsonencoder import TimJsonEncoder +if TYPE_CHECKING: + from timApp.messaging.timMessage.internalmessage_models import ( + InternalMessageReadReceipt, + ) + from timApp.user.consentchange import ConsentChange + from timApp.lecture.lecture import Lecture + from timApp.lecture.lectureanswer import LectureAnswer + from timApp.lecture.message import Message + from timApp.lecture.questionactivity import QuestionActivity + from timApp.lecture.useractivity import Useractivity + from timApp.velp.annotation_model import Annotation + from timApp.velp.velp_models import Velp + + ItemOrBlock = Union[ItemBase, Block] maxdate = datetime.max.replace(tzinfo=timezone.utc) @@ -247,57 +266,52 @@ class User(db.Model, TimeStampMixin, SCIMEntity): """ __tablename__ = "useraccount" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """User identifier.""" - name = mapped_column(db.Text, nullable=False, unique=True) + name: Mapped[str] = mapped_column(unique=True) """User name (not full name). Used to identify the user and during log-in.""" - given_name = mapped_column(db.Text) + given_name: Mapped[Optional[str]] """User's given name.""" - last_name = mapped_column(db.Text) + last_name: Mapped[Optional[str]] """User's last name.""" - real_name = mapped_column(db.Text) + real_name: Mapped[Optional[str]] """Real (full) name. This may be in the form "Lastname Firstname" or "Firstname Lastname".""" - _email = mapped_column("email", db.Text, unique=True) + _email: Mapped[str] = mapped_column("email", unique=True) """Email address.""" - prefs = mapped_column(db.Text) + prefs: Mapped[Optional[str]] """Preferences as a JSON string.""" - pass_ = mapped_column("pass", db.Text) + pass_: Mapped[Optional[str]] = mapped_column("pass") """Password hashed with bcrypt.""" - consent = mapped_column(db.Enum(Consent), nullable=True) + consent: Mapped[Optional[Consent]] """Current consent for cookie/data collection.""" - origin = mapped_column(db.Enum(UserOrigin), nullable=True) + origin: Mapped[Optional[UserOrigin]] """How the user registered to TIM.""" - uniquecodes = db.relationship( - "PersonalUniqueCode", + uniquecodes: Mapped[Dict[Tuple[int, str], "PersonalUniqueCode"]] = relationship( back_populates="user", - collection_class=attribute_mapped_collection("user_collection_key"), + collection_class=attribute_keyed_dict("user_collection_key"), ) """Personal unique codes used to identify the user via Haka Identity Provider.""" - internalmessage_readreceipt = db.relationship( - "InternalMessageReadReceipt", back_populates="user" - ) # : InternalMessageReadReceipt | None + internalmessage_readreceipt: Mapped[ + Optional["InternalMessageReadReceipt"] + ] = relationship(back_populates="user") """User's read receipts for internal messages.""" - primary_email_contact = db.relationship( - UserContact, + primary_email_contact: Mapped["UserContact"] = relationship( primaryjoin=(id == UserContact.user_id) & (UserContact.primary == PrimaryContact.true) & (UserContact.channel == Channel.EMAIL), - lazy="select", - uselist=False, overlaps="user, contacts", ) """ @@ -306,34 +320,30 @@ class User(db.Model, TimeStampMixin, SCIMEntity): The primary contact is the preferred email address that the user wants to receive notifications from TIM. """ - def _get_email(self) -> str: + @hybrid_property + def email(self) -> str: + """ + User's primary email address. + + This is the address the user can log in with and receive notifications from TIM. + """ return self._email + @email.inplace.setter def _set_email(self, value: str) -> None: self.update_email(value) - # Decorators don't work with mypy yet - # See https://github.com/dropbox/sqlalchemy-stubs/issues/98 - email = hybrid_property(_get_email, _set_email) - """ - User's primary email address. - - This is the address the user can log in with and receive notifications from TIM. - """ - - consents = db.relationship("ConsentChange", back_populates="user", lazy="select") + consents: Mapped[List["ConsentChange"]] = relationship(back_populates="user") """User's consent changes.""" - contacts = db.relationship( - "UserContact", + contacts: Mapped[List["UserContact"]] = relationship( back_populates="user", - lazy="select", overlaps="primary_email_contact", cascade_backrefs=False, - ) # : list[UserContact] + ) """User's contacts.""" - notifications = db.relationship( + notifications: DynamicMapped["Notification"] = relationship( "Notification", back_populates="user", lazy="dynamic", @@ -341,130 +351,126 @@ def _set_email(self, value: str) -> None: ) """Notification settings for the user. Represents what notifications the user wants to receive.""" - groups = db.relationship( - UserGroup, - UserGroupMember.__table__, + groups: Mapped[List["UserGroup"]] = relationship( + secondary=UserGroupMember.__table__, primaryjoin=(id == UserGroupMember.user_id) & membership_current, back_populates="users", - lazy="select", overlaps="user, current_memberships, group, memberships, memberships_sel", - ) # : list[UserGroup] + ) """Current groups of the user is a member of.""" - groups_dyn = db.relationship( - UserGroup, - UserGroupMember.__table__, + groups_dyn: DynamicMapped["UserGroup"] = relationship( + secondary=UserGroupMember.__table__, primaryjoin=id == UserGroupMember.user_id, lazy="dynamic", overlaps="group, groups, user, users, current_memberships, memberships, memberships_sel", ) """All groups of the user as a dynamic query.""" - groups_inactive = db.relationship( - UserGroup, - UserGroupMember.__table__, + groups_inactive: DynamicMapped["UserGroup"] = relationship( + secondary=UserGroupMember.__table__, primaryjoin=(id == UserGroupMember.user_id) & membership_deleted, lazy="dynamic", overlaps="group, groups, groups_dyn, user, users, current_memberships, memberships, memberships_sel", ) """All groups the user is no longer a member of as a dynamic query.""" - memberships_dyn = db.relationship( - UserGroupMember, + memberships_dyn: DynamicMapped["UserGroupMember"] = relationship( foreign_keys="UserGroupMember.user_id", lazy="dynamic", overlaps="groups, groups_dyn, groups_inactive, user, users", ) """User's group memberships as a dynamic query.""" - memberships = db.relationship( - UserGroupMember, + memberships: Mapped[List["UserGroupMember"]] = relationship( foreign_keys="UserGroupMember.user_id", overlaps="groups_inactive, memberships_dyn, user, users", - ) # : list[UserGroupMember] + ) """All user's group memberships.""" - active_memberships = db.relationship( - UserGroupMember, + active_memberships: Mapped[Dict[int, "UserGroupMember"]] = relationship( primaryjoin=(id == UserGroupMember.user_id) & membership_current, - collection_class=attribute_mapped_collection("usergroup_id"), + collection_class=attribute_keyed_dict("usergroup_id"), overlaps="groups, groups_dyn, groups_inactive, memberships, memberships_dyn, user, users", ) """Active group memberships mapped by user group ID.""" - lectures = db.relationship( - "Lecture", + lectures: Mapped[List["Lecture"]] = relationship( secondary=LectureUsers.__table__, back_populates="users", - lazy="select", ) """Lectures that the user is attending at the moment.""" - owned_lectures = db.relationship("Lecture", back_populates="owner", lazy="dynamic") + owned_lectures: DynamicMapped["Lecture"] = relationship( + back_populates="owner", lazy="dynamic" + ) """Lectures that the user has created.""" - lectureanswers = db.relationship( - "LectureAnswer", back_populates="user", lazy="dynamic" + lectureanswers: DynamicMapped["LectureAnswer"] = relationship( + back_populates="user", lazy="dynamic" ) """Lecture answers that the user sent to lectures as a dynamic query.""" - messages = db.relationship("Message", back_populates="user", lazy="dynamic") + messages: DynamicMapped["Message"] = relationship( + back_populates="user", lazy="dynamic" + ) """Lecture messages that the user sent to lectures as a dynamic query.""" - questionactivity = db.relationship( - "QuestionActivity", back_populates="user", lazy="select", cascade_backrefs=False + questionactivity: Mapped[List["QuestionActivity"]] = relationship( + back_populates="user" ) """User's activity on lecture questions.""" - useractivity = db.relationship("Useractivity", back_populates="user", lazy="select") + useractivity: Mapped[List["Useractivity"]] = relationship(back_populates="user") """User's activity during lectures.""" - answers = db.relationship( - "Answer", + answers: DynamicMapped["Answer"] = relationship( secondary=UserAnswer.__table__, back_populates="users", lazy="dynamic", overlaps="users_all", - cascade_backrefs=False, ) """User's answers to tasks as a dynamic query.""" - annotations = db.relationship( - "Annotation", back_populates="annotator", lazy="dynamic" + annotations: DynamicMapped["Annotation"] = relationship( + back_populates="annotator", lazy="dynamic" ) """User's task annotations as a dynamic query.""" - velps = db.relationship("Velp", back_populates="creator", lazy="dynamic") + velps: DynamicMapped["Velp"] = relationship( + back_populates="creator", lazy="dynamic" + ) """Velps created by the user as a dynamic query.""" - sessions = db.relationship( - "UserSession", back_populates="user", lazy="select", cascade_backrefs=False - ) # : list[UserSession] + sessions: Mapped[List["UserSession"]] = relationship(back_populates="user") """All user's sessions as a dynamic query.""" - active_sessions = db.relationship( - "UserSession", + active_sessions: Mapped[Dict[str, "UserSession"]] = relationship( primaryjoin=(id == UserSession.user_id) & ~UserSession.expired, - collection_class=attribute_mapped_collection("session_id"), + collection_class=attribute_keyed_dict("session_id"), overlaps="sessions, user", - ) # : MutableMapping[str, UserSession] + ) """Active sessions mapped by the session ID.""" # Used for copying - notifications_alt = db.relationship("Notification", overlaps="notifications, user") - owned_lectures_alt = db.relationship("Lecture", overlaps="owned_lectures, owner") - lectureanswers_alt = db.relationship( - "LectureAnswer", overlaps="lectureanswers, user" + notifications_alt: Mapped[List["Notification"]] = relationship( + overlaps="notifications, user" + ) + owned_lectures_alt: Mapped[List["Lecture"]] = relationship( + overlaps="owned_lectures, owner" ) - messages_alt = db.relationship("Message", overlaps="messages, user") - answers_alt = db.relationship( - "Answer", + lectureanswers_alt: Mapped[List["LectureAnswer"]] = relationship( + overlaps="lectureanswers, user" + ) + messages_alt: Mapped[List["Message"]] = relationship(overlaps="messages, user") + answers_alt: Mapped[List["Answer"]] = relationship( secondary=UserAnswer.__table__, overlaps="answers, users", - cascade_backrefs=False, ) - annotations_alt = db.relationship("Annotation", overlaps="annotations, annotator") - velps_alt = db.relationship("Velp", overlaps="velps, creator") + annotations_alt: Mapped[List["Annotation"]] = relationship( + overlaps="annotations, annotator" + ) + velps_alt: Mapped[List["Velp"]] = relationship(overlaps="velps, creator") def update_email( self, diff --git a/timApp/user/usercontact.py b/timApp/user/usercontact.py index 5a3fcf80b7..c66949bccb 100644 --- a/timApp/user/usercontact.py +++ b/timApp/user/usercontact.py @@ -1,10 +1,16 @@ from enum import Enum +from typing import Optional, TYPE_CHECKING, List -from sqlalchemy.orm import mapped_column +from sqlalchemy import UniqueConstraint +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.messaging.messagelist.listinfo import Channel from timApp.timdb.sqa import db +if TYPE_CHECKING: + from timApp.user.user import User + from timApp.user.verification.verification import ContactAddVerification + class ContactOrigin(Enum): """Indicates what system added the contact to the user. @@ -35,13 +41,12 @@ class UserContact(db.Model): __tablename__ = "usercontact" - __table_args__ = ( # A user should not have the same contact for the channel # Different users are fine though - db.UniqueConstraint("user_id", "contact", "channel", name="user_contact_uc"), + UniqueConstraint("user_id", "contact", "channel", name="user_contact_uc"), # The same user cannot have multiple primary contacts for the same channel - db.UniqueConstraint( + UniqueConstraint( "user_id", "channel", "primary", @@ -49,7 +54,7 @@ class UserContact(db.Model): initially="DEFERRED", # Allow for easy swapping of primary email within the same transaction ), # Multiple users cannot have the same contact as primary - db.UniqueConstraint( + UniqueConstraint( "channel", "contact", "primary", @@ -57,35 +62,32 @@ class UserContact(db.Model): ), ) - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) """Which user owns this contact information.""" - contact = mapped_column(db.Text, nullable=False) + contact: Mapped[str] """Contact identifier for a channel.""" - channel = mapped_column(db.Enum(Channel), nullable=False) + channel: Mapped[Channel] """Channel the contact information points to.""" - verified = mapped_column(db.Boolean, nullable=False, default=False) + verified: Mapped[bool] = mapped_column(default=False) """Whether this contact info is verified by the user. If False, the user has made a claim for a contact info, but has not yet verified it's ownership.""" - primary = mapped_column(db.Enum(PrimaryContact)) + primary: Mapped[Optional[PrimaryContact]] """Whether the contact is primary for the user""" - contact_origin = mapped_column( - db.Enum(ContactOrigin), nullable=False - ) # : ContactOrigin + contact_origin: Mapped[ContactOrigin] """How the contact was added.""" - user = db.relationship("User", back_populates="contacts", lazy="select") + user: Mapped["User"] = relationship(back_populates="contacts") """User that the contact is associated with.""" - _verifications = db.relationship( - "ContactAddVerification", + _verifications: Mapped[List["ContactAddVerification"]] = relationship( back_populates="contact", cascade="all, delete-orphan", ) diff --git a/timApp/user/usergroup.py b/timApp/user/usergroup.py index 7b9643e79a..371cec443c 100644 --- a/timApp/user/usergroup.py +++ b/timApp/user/usergroup.py @@ -1,11 +1,18 @@ from __future__ import annotations from functools import lru_cache -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, List, Dict, Tuple import attr from sqlalchemy import select -from sqlalchemy.orm import selectinload, mapped_column, attribute_mapped_collection +from sqlalchemy.orm import ( + selectinload, + mapped_column, + Mapped, + DynamicMapped, + relationship, + attribute_keyed_dict, +) from sqlalchemy.sql import Select from timApp.sisu.parse_display_name import parse_sisu_group_display_name @@ -27,6 +34,15 @@ if TYPE_CHECKING: from timApp.item.block import Block from timApp.document.docentry import DocEntry + from timApp.user.user import User + from timApp.auth.auth_models import BlockAccess + from timApp.readmark.readparagraph import ReadParagraph + from timApp.note.usernote import UserNote + from timApp.messaging.messagelist.messagelist_models import MessageListTimMember + from timApp.messaging.timMessage.internalmessage_models import ( + InternalMessageDisplay, + ) + # Prefix is no longer needed because scimusergroup determines the Sisu (SCIM) groups. SISU_GROUP_PREFIX = "" @@ -55,15 +71,14 @@ class UserGroup(db.Model, TimeStampMixin, SCIMEntity): """ __tablename__ = "usergroup" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """Usergroup identifier.""" - name = mapped_column(db.Text, nullable=False, unique=True) + name: Mapped[str] = mapped_column(unique=True) """Usergroup name (textual identifier).""" - display_name = mapped_column(db.Text, nullable=True) + display_name: Mapped[Optional[str]] """Usergroup display name. Currently only used for storing certain Sisu course properties: - course code - period (P1...P5) @@ -75,77 +90,62 @@ class UserGroup(db.Model, TimeStampMixin, SCIMEntity): def scim_display_name(self): return self.display_name - users = db.relationship( - "User", - UserGroupMember.__table__, + users: Mapped[List["User"]] = relationship( + secondary=UserGroupMember.__table__, primaryjoin=(id == UserGroupMember.usergroup_id) & membership_current, secondaryjoin="UserGroupMember.user_id == User.id", back_populates="groups", overlaps="group, user", ) - memberships = db.relationship( - UserGroupMember, + memberships: DynamicMapped["UserGroupMember"] = relationship( back_populates="group", lazy="dynamic", overlaps="users", ) - memberships_sel = db.relationship( - UserGroupMember, + memberships_sel: Mapped[List["UserGroupMember"]] = relationship( back_populates="group", cascade="all, delete-orphan", overlaps="memberships, users", ) - current_memberships = db.relationship( - UserGroupMember, + current_memberships: Mapped[Dict[int, "UserGroupMember"]] = relationship( primaryjoin=(id == UserGroupMember.usergroup_id) & membership_current, - collection_class=attribute_mapped_collection("user_id"), + collection_class=attribute_keyed_dict("user_id"), back_populates="group", overlaps="memberships, memberships_sel, users", - ) # : dict[int, UserGroupMember] - accesses = db.relationship( - "BlockAccess", - back_populates="usergroup", - lazy="dynamic", - cascade_backrefs=False, ) - accesses_alt = db.relationship( - "BlockAccess", - collection_class=attribute_mapped_collection("group_collection_key"), + accesses: DynamicMapped["BlockAccess"] = relationship( + back_populates="usergroup", lazy="dynamic" + ) + accesses_alt: Mapped[Dict[Tuple[int, int], "BlockAccess"]] = relationship( + collection_class=attribute_keyed_dict("group_collection_key"), cascade="all, delete-orphan", overlaps="accesses, usergroup", - cascade_backrefs=False, - ) # : dict[tuple[int, int], BlockAccess] - readparagraphs = db.relationship( - "ReadParagraph", back_populates="usergroup", lazy="dynamic" ) - readparagraphs_alt = db.relationship( - "ReadParagraph", - overlaps="readparagraphs, usergroup", + readparagraphs: DynamicMapped["ReadParagraph"] = relationship( + back_populates="usergroup", lazy="dynamic" ) - notes = db.relationship( - "UserNote", back_populates="usergroup", lazy="dynamic", cascade_backrefs=False + readparagraphs_alt: Mapped[List["ReadParagraph"]] = relationship( + overlaps="readparagraphs, usergroup" ) - notes_alt = db.relationship("UserNote", overlaps="notes, usergroup") + notes: DynamicMapped["UserNote"] = relationship( + back_populates="usergroup", lazy="dynamic" + ) + notes_alt: Mapped[List["UserNote"]] = relationship(overlaps="notes, usergroup") - admin_doc = db.relationship( - "Block", - secondary=UserGroupDoc.__table__, - lazy="select", - uselist=False, - ) # : Block + admin_doc: Mapped[Optional["Block"]] = relationship( + secondary=UserGroupDoc.__table__ + ) # For groups created from SCIM API - external_id = db.relationship( - "ScimUserGroup", lazy="select", uselist=False - ) # : ScimUserGroup + external_id: Mapped[Optional["ScimUserGroup"]] = relationship() - messagelist_membership = db.relationship( - "MessageListTimMember", back_populates="user_group" - ) # : list[MessageListTimMember] + messagelist_membership: Mapped[List["MessageListTimMember"]] = relationship( + back_populates="user_group" + ) - internalmessage_display = db.relationship( - "InternalMessageDisplay", back_populates="usergroup", cascade_backrefs=False - ) # : InternalMessageDisplay | None + internalmessage_display: Mapped[Optional["InternalMessageDisplay"]] = relationship( + back_populates="usergroup" + ) def __repr__(self): return f"" diff --git a/timApp/user/usergroupdoc.py b/timApp/user/usergroupdoc.py index 9dda0389e6..03a993ebce 100644 --- a/timApp/user/usergroupdoc.py +++ b/timApp/user/usergroupdoc.py @@ -1,4 +1,4 @@ -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db @@ -9,7 +9,8 @@ class UserGroupDoc(db.Model): """ __tablename__ = "usergroupdoc" - - group_id = mapped_column(db.Integer, db.ForeignKey("usergroup.id"), primary_key=True) - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) + group_id: Mapped[int] = mapped_column( + db.ForeignKey("usergroup.id"), primary_key=True + ) + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) diff --git a/timApp/user/usergroupmember.py b/timApp/user/usergroupmember.py index 7826e3e17a..301af45bb6 100644 --- a/timApp/user/usergroupmember.py +++ b/timApp/user/usergroupmember.py @@ -11,13 +11,19 @@ All this information is contained in :class:`UserGroupMember` which links a user to the group they belong to. """ from datetime import timedelta +from typing import Optional, TYPE_CHECKING from sqlalchemy import func -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.util.utils import get_current_time +if TYPE_CHECKING: + from timApp.user.user import User + from timApp.user.usergroup import UserGroup + class UserGroupMember(db.Model): """ @@ -25,52 +31,41 @@ class UserGroupMember(db.Model): """ __tablename__ = "usergroupmember" - - usergroup_id = mapped_column( - db.Integer, db.ForeignKey("usergroup.id"), primary_key=True + usergroup_id: Mapped[int] = mapped_column( + db.ForeignKey("usergroup.id"), primary_key=True ) """ID of the usergroup the member belongs to.""" - user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + user_id: Mapped[int] = mapped_column( + db.ForeignKey("useraccount.id"), primary_key=True ) """ID of the user that belongs to the usergroup.""" - membership_end = mapped_column(db.DateTime(timezone=True)) + membership_end: Mapped[Optional[datetime_tz]] """Timestamp for when the membership ended. .. note:: The timestamp is used to determine soft deletion. If the end timestamp is present, the user is considered deleted from the group. """ - membership_added = mapped_column( - db.DateTime(timezone=True), default=get_current_time - ) + membership_added: Mapped[datetime_tz] = mapped_column(default=get_current_time) """Timestamp for when the user was last time added as the active member. .. note:: The timestamp is used **for logging purposes only**. In other words, it is not used to determine soft deletion or other membership state. """ - added_by = mapped_column(db.Integer, db.ForeignKey("useraccount.id")) + added_by: Mapped[Optional[int]] = mapped_column(db.ForeignKey("useraccount.id")) """User ID of the user who added the membership.""" - user = db.relationship( - "User", - foreign_keys=[user_id], - ) + user: Mapped["User"] = relationship(foreign_keys=[user_id]) """User that this membership belongs to. Relationship of the :attr:`user_id` column.""" - adder = db.relationship( - "User", - foreign_keys=[added_by], - ) + adder: Mapped[Optional["User"]] = relationship(foreign_keys=[added_by]) """User that added this membership. Relationship of the :attr:`added_by` column.""" - group = db.relationship( - "UserGroup", - ) + group: Mapped["UserGroup"] = relationship() """Group that this membership belongs to. Relationship of the :attr:`usergroup_id` column.""" def set_expired( diff --git a/timApp/user/verification/verification.py b/timApp/user/verification/verification.py index 5b298ef8ed..9ea1f4ca35 100644 --- a/timApp/user/verification/verification.py +++ b/timApp/user/verification/verification.py @@ -5,10 +5,11 @@ from flask import render_template_string, url_for from sqlalchemy import select -from sqlalchemy.orm import load_only, mapped_column +from sqlalchemy.orm import load_only, mapped_column, Mapped, relationship from timApp.document.docentry import DocEntry from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.user.user import User from timApp.user.usercontact import UserContact, PrimaryContact from timApp.util.utils import get_current_time @@ -50,24 +51,23 @@ class Verification(db.Model): verification.""" __tablename__ = "verification" - - token = mapped_column(db.Text, primary_key=True) + token: Mapped[str] = mapped_column(primary_key=True) """Verification token used for action verification""" - type = mapped_column(db.Enum(VerificationType), primary_key=True) # : VerificationType + type: Mapped[VerificationType] = mapped_column(primary_key=True) """The type of verification, see VerificationType class for details.""" - user_id = mapped_column(db.Integer, db.ForeignKey("useraccount.id"), nullable=False) + user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) """User that can react to verification request.""" - requested_at = mapped_column(db.DateTime(timezone=True)) + requested_at: Mapped[Optional[datetime_tz]] """When a verification has been added to db, pending sending to a user.""" - reacted_at = mapped_column(db.DateTime(timezone=True)) + reacted_at: Mapped[Optional[datetime_tz]] """When the user reacted to verification request.""" - user = db.relationship("User", lazy="select") # : User + user: Mapped["User"] = relationship() """User that can react to verification request.""" @property @@ -91,7 +91,7 @@ class ContactAddVerification(Verification): contact = db.relationship( "UserContact", lazy="select", uselist=False - ) # : UserContact | None + ) # : UserContact | None """Contact to verify.""" @property diff --git a/timApp/velp/annotation_model.py b/timApp/velp/annotation_model.py index dc145b3739..0fba07b462 100644 --- a/timApp/velp/annotation_model.py +++ b/timApp/velp/annotation_model.py @@ -1,10 +1,17 @@ import json from dataclasses import dataclass from datetime import datetime +from typing import Optional, TYPE_CHECKING, List -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz + +if TYPE_CHECKING: + from timApp.user.user import User + from timApp.answer.answer import Answer + from timApp.velp.velp_models import AnnotationComment, VelpVersion, VelpContent @dataclass @@ -46,36 +53,29 @@ class Annotation(db.Model): """ __tablename__ = "annotation" - - - id = mapped_column(db.Integer, primary_key=True) + + id: Mapped[int] = mapped_column(primary_key=True) """Annotation identifier.""" - velp_version_id = mapped_column( - db.Integer, db.ForeignKey("velpversion.id"), nullable=False - ) + velp_version_id: Mapped[int] = mapped_column(db.ForeignKey("velpversion.id")) """Id of the velp that has been used for this annotation.""" - annotator_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), nullable=False - ) + annotator_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) """Id of the User who created the annotation.""" - points = mapped_column(db.Float) + points: Mapped[Optional[float]] """Points associated with the annotation.""" - creation_time = mapped_column( - db.DateTime(timezone=True), nullable=False, default=datetime.utcnow - ) + creation_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) """Creation time.""" - valid_from = mapped_column(db.DateTime(timezone=True), default=datetime.utcnow) + valid_from: Mapped[Optional[datetime_tz]] = mapped_column(default=datetime.utcnow) """Since when should this annotation be valid.""" - valid_until = mapped_column(db.DateTime(timezone=True)) + valid_until: Mapped[Optional[datetime_tz]] """Until when should this annotation be valid.""" - visible_to = mapped_column(db.Integer) + visible_to: Mapped[Optional[int]] """Who should this annotation be visible to. Possible values are denoted by AnnotationVisibility enum: @@ -87,63 +87,64 @@ class Annotation(db.Model): """ - document_id = mapped_column(db.Integer, db.ForeignKey("block.id")) + document_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) """Id of the document in case this is a paragraph annotation.""" - answer_id = mapped_column(db.Integer, db.ForeignKey("answer.id")) + answer_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("answer.id")) """Id of the Answer in case this is an answer annotation.""" - paragraph_id_start = mapped_column(db.Text) + paragraph_id_start: Mapped[Optional[str]] """The id of the paragraph where this annotation starts from (in case this is a paragraph annotation).""" - paragraph_id_end = mapped_column(db.Text) + paragraph_id_end: Mapped[Optional[int]] """The id of the paragraph where this annotation ends (in case this is a paragraph annotation).""" - offset_start = mapped_column(db.Integer) + offset_start: Mapped[Optional[int]] """Positional information about the annotation.""" - node_start = mapped_column(db.Integer) + node_start: Mapped[Optional[int]] """Positional information about the annotation.""" - depth_start = mapped_column(db.Integer) + depth_start: Mapped[Optional[int]] """Positional information about the annotation.""" - offset_end = mapped_column(db.Integer) + offset_end: Mapped[Optional[int]] """Positional information about the annotation.""" - node_end = mapped_column(db.Integer) + node_end: Mapped[Optional[int]] """Positional information about the annotation.""" - depth_end = mapped_column(db.Integer) + depth_end: Mapped[Optional[int]] """Positional information about the annotation.""" - hash_start = mapped_column(db.Text) + hash_start: Mapped[Optional[str]] """Positional information about the annotation.""" - hash_end = mapped_column(db.Text) + hash_end: Mapped[Optional[str]] """Positional information about the annotation.""" - color = mapped_column(db.Text) + color: Mapped[Optional[str]] """Color for the annotation.""" - element_path_start = mapped_column(db.Text) + element_path_start: Mapped[Optional[str]] """Positional information about the annotation.""" - element_path_end = mapped_column(db.Text) + element_path_end: Mapped[Optional[str]] """Positional information about the annotation.""" - draw_data = mapped_column(db.Text) + draw_data: Mapped[Optional[str]] """Drawing information about the annotation (for annotations on images).""" - style = mapped_column(db.Integer) + style: Mapped[Optional[int]] """Appearance of the annotation""" - annotator = db.relationship("User", back_populates="annotations") - answer = db.relationship("Answer", back_populates="annotations") - comments = db.relationship("AnnotationComment", order_by="AnnotationComment.id") - velp_version = db.relationship("VelpVersion") - velp_content = db.relationship( - "VelpContent", + annotator: Mapped["User"] = relationship(back_populates="annotations") + answer: Mapped[Optional["Answer"]] = relationship(back_populates="annotations") + comments: Mapped[List["AnnotationComment"]] = relationship( + order_by="AnnotationComment.id" + ) + velp_version: Mapped["VelpVersion"] = relationship() + velp_content: Mapped["VelpContent"] = relationship( primaryjoin="VelpContent.version_id == foreign(Annotation.velp_version_id)", overlaps="velp_version", ) diff --git a/timApp/velp/velp_models.py b/timApp/velp/velp_models.py index 07733caf53..0c6455c650 100644 --- a/timApp/velp/velp_models.py +++ b/timApp/velp/velp_models.py @@ -1,56 +1,54 @@ """Defines all data models related to velps.""" from datetime import datetime +from typing import Optional, TYPE_CHECKING, Dict, List -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import mapped_column, Mapped, relationship, attribute_keyed_dict from sqlalchemy.orm.collections import attribute_mapped_collection # type: ignore +from timApp.item.block import Block from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz + +if TYPE_CHECKING: + from timApp.user.user import User class VelpContent(db.Model): """The actual content of a Velp.""" __tablename__ = "velpcontent" - - version_id = mapped_column( - db.Integer, db.ForeignKey("velpversion.id"), primary_key=True + version_id: Mapped[int] = mapped_column( + db.ForeignKey("velpversion.id"), primary_key=True ) - language_id = mapped_column(db.Text, primary_key=True) - content = mapped_column(db.Text) - default_comment = mapped_column(db.Text) + language_id: Mapped[str] = mapped_column(primary_key=True) + content: Mapped[Optional[str]] + default_comment: Mapped[Optional[str]] - velp_version = db.relationship("VelpVersion") + velp_version: Mapped["VelpVersion"] = relationship() class AnnotationComment(db.Model): """A comment in an Annotation.""" __tablename__ = "annotationcomment" - - id = mapped_column(db.Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) """Comment identifier.""" - annotation_id = mapped_column( - db.Integer, db.ForeignKey("annotation.id"), nullable=False - ) + annotation_id: Mapped[int] = mapped_column(db.ForeignKey("annotation.id")) """Annotation id.""" - comment_time = mapped_column( - db.DateTime(timezone=True), nullable=False, default=datetime.utcnow - ) + comment_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) """Comment timestamp.""" - commenter_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), nullable=False - ) + commenter_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) """Commenter user id.""" - content = mapped_column(db.Text) + content: Mapped[Optional[str]] """Comment text.""" - commenter = db.relationship("User") + commenter: Mapped["User"] = relationship() def to_json(self) -> dict: return { @@ -66,61 +64,52 @@ class LabelInVelp(db.Model): """Associates VelpLabels with Velps.""" __tablename__ = "labelinvelp" - - label_id = mapped_column( - db.Integer, db.ForeignKey("velplabel.id"), primary_key=True + label_id: Mapped[int] = mapped_column( + db.ForeignKey("velplabel.id"), primary_key=True ) - velp_id = mapped_column(db.Integer, db.ForeignKey("velp.id"), primary_key=True) + velp_id: Mapped[int] = mapped_column(db.ForeignKey("velp.id"), primary_key=True) class VelpInGroup(db.Model): __tablename__ = "velpingroup" - - velp_group_id = mapped_column( - db.Integer, db.ForeignKey("velpgroup.id"), primary_key=True + velp_group_id: Mapped[int] = mapped_column( + db.ForeignKey("velpgroup.id"), primary_key=True ) - velp_id = mapped_column(db.Integer, db.ForeignKey("velp.id"), primary_key=True) + velp_id: Mapped[int] = mapped_column(db.ForeignKey("velp.id"), primary_key=True) class Velp(db.Model): """A Velp is a kind of category for Annotations and is visually represented by a Post-it note.""" __tablename__ = "velp" - - id = mapped_column(db.Integer, primary_key=True) - creator_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), nullable=False - ) - creation_time = mapped_column( - db.DateTime(timezone=True), nullable=False, default=datetime.utcnow - ) - default_points = mapped_column(db.Float) - valid_from = mapped_column(db.DateTime(timezone=True), default=datetime.utcnow) - valid_until = mapped_column(db.DateTime(timezone=True)) - color = mapped_column(db.Text) - visible_to = mapped_column(db.Integer, nullable=False) - style = mapped_column(db.Integer) - - creator = db.relationship("User", back_populates="velps") - labels = db.relationship( - "VelpLabel", + id: Mapped[int] = mapped_column(primary_key=True) + creator_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + creation_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) + default_points: Mapped[Optional[float]] + valid_from: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) + valid_until: Mapped[Optional[datetime_tz]] + color: Mapped[Optional[str]] + visible_to: Mapped[int] + style: Mapped[Optional[int]] + + creator: Mapped["User"] = relationship(back_populates="velps") + labels: Mapped[Dict[int, "VelpLabel"]] = relationship( back_populates="velps", secondary=LabelInVelp.__table__, - collection_class=attribute_mapped_collection("id"), + collection_class=attribute_keyed_dict("id"), ) - groups = db.relationship( - "VelpGroup", + groups: Mapped[Dict[int, "VelpGroup"]] = relationship( back_populates="velps", secondary=VelpInGroup.__table__, collection_class=attribute_mapped_collection("id"), cascade="all", ) - velp_versions = db.relationship( - "VelpVersion", order_by="VelpVersion.id.desc()" - ) # : list["VelpVersion"] + velp_versions: Mapped[List["VelpVersion"]] = relationship( + order_by="VelpVersion.id.desc()" + ) def to_json(self) -> dict: vv = self.velp_versions[0] @@ -144,31 +133,21 @@ class VelpGroup(db.Model): """Represents a group of Velps.""" __tablename__ = "velpgroup" - - id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - name = mapped_column(db.Text) - creation_time = mapped_column( - db.DateTime(timezone=True), nullable=False, default=datetime.utcnow - ) - valid_from = mapped_column(db.DateTime(timezone=True), default=datetime.utcnow) - valid_until = mapped_column(db.DateTime(timezone=True)) - default_group = mapped_column(db.Boolean, default=False) + id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + name: Mapped[Optional[str]] + creation_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) + valid_from: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) + valid_until: Mapped[Optional[datetime_tz]] + default_group: Mapped[bool] = mapped_column(default=False) - velps = db.relationship( - "Velp", + velps: Mapped[Dict[int, "Velp"]] = db.relationship( back_populates="groups", secondary=VelpInGroup.__table__, - collection_class=attribute_mapped_collection("id"), + collection_class=attribute_keyed_dict("id"), cascade="all", ) - block = db.relationship( - "Block", - lazy="selectin", - ) # : Block - # docentry = db.relationship( - # 'DocEntry', - # ) + block: Mapped["Block"] = db.relationship(lazy="joined") def to_json(self) -> dict: return { @@ -180,44 +159,37 @@ def to_json(self) -> dict: class VelpGroupDefaults(db.Model): __tablename__ = "velpgroupdefaults" - - - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - target_type = mapped_column( - db.Integer, nullable=False - ) # 0 = document, 1 = paragraph, 2 = area - target_id = mapped_column(db.Text, primary_key=True) - velp_group_id = mapped_column( - db.Integer, db.ForeignKey("velpgroup.id"), primary_key=True + + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + target_id: Mapped[str] = mapped_column(primary_key=True) + velp_group_id: Mapped[int] = mapped_column( + db.ForeignKey("velpgroup.id"), primary_key=True ) - selected = mapped_column(db.Boolean, default=False) + target_type: Mapped[int] # 0 = document, 1 = paragraph, 2 = area + selected: Mapped[bool] = mapped_column(default=False) class VelpGroupLabel(db.Model): """Currently not used (0 rows in production DB as of 5th July 2018).""" __tablename__ = "velpgrouplabel" - - id = mapped_column(db.Integer, primary_key=True) - content = mapped_column(db.Text, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + content: Mapped[str] class VelpGroupSelection(db.Model): __tablename__ = "velpgroupselection" - - user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + user_id: Mapped[int] = mapped_column( + db.ForeignKey("useraccount.id"), primary_key=True ) - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - target_type = mapped_column( - db.Integer, nullable=False - ) # 0 = document, 1 = paragraph, 2 = area - target_id = mapped_column(db.Text, primary_key=True) - selected = mapped_column(db.Boolean, default=False) - velp_group_id = mapped_column( - db.Integer, db.ForeignKey("velpgroup.id"), primary_key=True + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + target_id: Mapped[str] = mapped_column(primary_key=True) + target_type: Mapped[int] # 0 = document, 1 = paragraph, 2 = area + selected: Mapped[bool] = mapped_column(default=False) + velp_group_id: Mapped[int] = mapped_column( + db.ForeignKey("velpgroup.id"), primary_key=True ) @@ -229,14 +201,13 @@ class VelpGroupsInDocument(db.Model): """ __tablename__ = "velpgroupsindocument" - - user_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), primary_key=True + user_id: Mapped[int] = mapped_column( + db.ForeignKey("useraccount.id"), primary_key=True ) - doc_id = mapped_column(db.Integer, db.ForeignKey("block.id"), primary_key=True) - velp_group_id = mapped_column( - db.Integer, db.ForeignKey("velpgroup.id"), primary_key=True + doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + velp_group_id: Mapped[int] = mapped_column( + db.ForeignKey("velpgroup.id"), primary_key=True ) @@ -244,34 +215,29 @@ class VelpLabel(db.Model): """A label that can be assigned to a Velp.""" __tablename__ = "velplabel" - - id = mapped_column(db.Integer, primary_key=True) - # TODO make not nullable - creator_id = mapped_column( - db.Integer, db.ForeignKey("useraccount.id"), nullable=True - ) + id: Mapped[int] = mapped_column(primary_key=True) + # TODO make not optional + creator_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("useraccount.id")) - creator = db.relationship("User") - velps = db.relationship( - "Velp", + creator: Mapped[Optional["User"]] = relationship() + velps: Mapped[Dict[int, "Velp"]] = relationship( back_populates="labels", secondary=LabelInVelp.__table__, - collection_class=attribute_mapped_collection("id"), + collection_class=attribute_keyed_dict("id"), ) class VelpLabelContent(db.Model): __tablename__ = "velplabelcontent" - - velplabel_id = mapped_column( - db.Integer, db.ForeignKey("velplabel.id"), primary_key=True + velplabel_id: Mapped[int] = mapped_column( + db.ForeignKey("velplabel.id"), primary_key=True ) - language_id = mapped_column(db.Text, primary_key=True) - content = mapped_column(db.Text) + language_id: Mapped[str] = mapped_column(primary_key=True) + content: Mapped[Optional[str]] - velplabel = db.relationship("VelpLabel") + velplabel: Mapped["VelpLabel"] = relationship() def to_json(self) -> dict: return { @@ -283,13 +249,10 @@ def to_json(self) -> dict: class VelpVersion(db.Model): __tablename__ = "velpversion" - - id = mapped_column(db.Integer, primary_key=True) - velp_id = mapped_column(db.Integer, db.ForeignKey("velp.id"), nullable=False) - modify_time = mapped_column( - db.DateTime(timezone=True), nullable=False, default=datetime.utcnow - ) + id: Mapped[int] = mapped_column(primary_key=True) + velp_id: Mapped[int] = mapped_column(db.ForeignKey("velp.id")) + modify_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) - velp = db.relationship("Velp", overlaps="velp_versions") # : Velp - content = db.relationship("VelpContent", overlaps="velp_version") # : list[VelpContent] + velp: Mapped["Velp"] = db.relationship("Velp", overlaps="velp_versions") + content: Mapped[List["VelpContent"]] = db.relationship(overlaps="velp_version") From b47c2a8f13ed391b83365889b86f72e54105d750 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Sat, 29 Jul 2023 10:49:18 +0300 Subject: [PATCH 09/34] Remove rarely used timtypes class --- timApp/document/changelog.py | 30 ++++++++++++++++-------------- timApp/document/docparagraph.py | 4 +--- timApp/document/document.py | 5 ++--- timApp/lecture/askedquestion.py | 10 +++++----- timApp/timtypes.py | 18 ------------------ timApp/user/user.py | 2 +- 6 files changed, 25 insertions(+), 44 deletions(-) delete mode 100644 timApp/timtypes.py diff --git a/timApp/document/changelog.py b/timApp/document/changelog.py index ee5dab6e01..3a50c50ac8 100644 --- a/timApp/document/changelog.py +++ b/timApp/document/changelog.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import List, Tuple, Optional +from typing import List, Tuple, Optional, TYPE_CHECKING, Union from sqlalchemy import select @@ -7,10 +7,13 @@ from timApp.document.changelogentry import ChangelogEntry from timApp.document.docparagraph import DocParagraph from timApp.timdb.sqa import db -from timApp.timtypes import UserOrGroup +if TYPE_CHECKING: + from timApp.user.user import User + from timApp.user.usergroup import UserGroup -def get_author_str(u: UserOrGroup, es: list[ChangelogEntry]): + +def get_author_str(u: Union["User", "UserGroup"], es: list[ChangelogEntry]): display_name = u.pretty_full_name num_changes = len(es) return display_name if num_changes <= 1 else f"{display_name} ({num_changes} edits)" @@ -18,9 +21,11 @@ def get_author_str(u: UserOrGroup, es: list[ChangelogEntry]): class AuthorInfo: def __init__( - self, user_map: dict[int, UserOrGroup], entries: dict[int, list[ChangelogEntry]] + self, + user_map: dict[int, Union["User", "UserGroup"]], + entries: dict[int, list[ChangelogEntry]], ) -> None: - self.authors: dict[UserOrGroup, list[ChangelogEntry]] = {} + self.authors: dict[Union["User", "UserGroup"], list[ChangelogEntry]] = {} for k, v in entries.items(): self.authors[user_map[k]] = v @@ -59,15 +64,12 @@ def get_authorinfo(self, pars: list[DocParagraph]) -> dict[str, AuthorInfo]: par_entry_map[e.par_id][e.group_id].append(e) User = timApp.user.user.User UserGroup = timApp.user.usergroup.UserGroup - result = ( - db.session.execute( - select(UserGroup, User) - .select_from(UserGroup) - .filter(UserGroup.id.in_(usergroup_ids)) - .outerjoin(User, User.name == UserGroup.name) - ) - .all() - ) # type: List[Tuple[UserGroup, Optional[User]]] + result = db.session.execute( + select(UserGroup, User) + .select_from(UserGroup) + .filter(UserGroup.id.in_(usergroup_ids)) + .outerjoin(User, User.name == UserGroup.name) + ).all() # type: List[Tuple[UserGroup, Optional[User]]] for ug, u in result: ug_obj_map[ug.id] = u or ug for i in par_ids: diff --git a/timApp/document/docparagraph.py b/timApp/document/docparagraph.py index 5166074439..a89a189a2e 100644 --- a/timApp/document/docparagraph.py +++ b/timApp/document/docparagraph.py @@ -30,7 +30,6 @@ AutoCounters, ) from timApp.timdb.exceptions import TimDbException, InvalidReferenceException -from timApp.timtypes import DocumentType from timApp.util.rndutils import get_rands_as_dict, SeedType from timApp.util.utils import count_chars_from_beginning, get_error_html, title_to_id from tim_common.dumboclient import DumboOptions, MathType, InputFormat @@ -616,7 +615,6 @@ def preload_htmls( with shelve.open(macro_cache_file) as c, shelve.open( heading_cache_file ) as hc: - # Basically we want the cache objects to be non-persistent, so we convert them to normal dicts # Find out better way if possible... for par in first_pars: @@ -1286,7 +1284,7 @@ def is_real_id(par_id: str | None): def create_reference( - doc: DocumentType, + doc: "Document", doc_id: int, par_id: str, translator: str | None = None, diff --git a/timApp/document/document.py b/timApp/document/document.py index 8429f2c544..2878946942 100644 --- a/timApp/document/document.py +++ b/timApp/document/document.py @@ -8,7 +8,7 @@ from pathlib import Path from tempfile import mkstemp from time import time -from typing import Iterable, Generator +from typing import Iterable, Generator, Optional from typing import TYPE_CHECKING from filelock import FileLock @@ -33,7 +33,6 @@ PreambleException, InvalidReferenceException, ) -from timApp.timtypes import DocInfoType from timApp.util.utils import get_error_html, trim_markdown, cache_folder_path from tim_common.html_sanitize import presanitize_html_body @@ -79,7 +78,7 @@ def __init__( # Cache for document settings. self.settings_cache: DocSettings | None = None # The corresponding DocInfo object. - self.docinfo: DocInfoType = None + self.docinfo: Optional["DocInfo"] = None # Cache for own settings; see get_own_settings self.own_settings = None # Whether preamble has been loaded diff --git a/timApp/lecture/askedquestion.py b/timApp/lecture/askedquestion.py index da9eeed124..0ab01279cd 100644 --- a/timApp/lecture/askedquestion.py +++ b/timApp/lecture/askedquestion.py @@ -10,10 +10,10 @@ from timApp.lecture.questionactivity import QuestionActivityKind, QuestionActivity from timApp.timdb.sqa import db from timApp.timdb.types import datetime_tz -from timApp.timtypes import UserType from timApp.util.utils import get_current_time if TYPE_CHECKING: + from timApp.user.user import User from timApp.lecture.askedjson import AskedJson from timApp.lecture.lecture import Lecture from timApp.lecture.lectureanswer import LectureAnswer @@ -62,10 +62,10 @@ def end_time(self) -> datetime | None: return None return self.asked_time + timedelta(seconds=timelimit) - def has_activity(self, kind: QuestionActivityKind, user: UserType): + def has_activity(self, kind: QuestionActivityKind, user: "User"): return self.questionactivity.filter_by(kind=kind, user_id=user.id).first() - def add_activity(self, kind: QuestionActivityKind, user: UserType): + def add_activity(self, kind: QuestionActivityKind, user: "User"): if self.has_activity(kind, user): return a = QuestionActivity(kind=kind, user=user, asked_question=self) @@ -100,7 +100,7 @@ def get_default_points(self): aj = self.asked_json.to_json() return aj["json"].get("defaultPoints", 0) - def build_answer_and_points(self, answer, u: UserType): + def build_answer_and_points(self, answer, u: "User"): """ Checks whether question was randomized If so, set question point input accordingly and expand answer to contain randomization data @@ -145,7 +145,7 @@ def get_asked_question(asked_id: int) -> AskedQuestion | None: @contextmanager -def user_activity_lock(user: UserType): +def user_activity_lock(user: "User"): db.session.execute(select(func.pg_advisory_xact_lock(user.id))) yield return diff --git a/timApp/timtypes.py b/timApp/timtypes.py deleted file mode 100644 index 8c4d785ecc..0000000000 --- a/timApp/timtypes.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Sometimes, using type annotations require imports that would cause circular imports. - -To work around it, this module defines aliases for the types that can be imported instead. - -TODO: These are broken at the moment after timApp reorganization. -""" - -from typing import Union - -# noinspection PyUnresolvedReferences -import timApp.document - -UserType = "timApp.timdb.models.user.User" -UserGroupType = "timApp.timdb.models.usergroup.UserGroup" -TranslationType = "timApp.timdb.models.translation.Translation" -DocInfoType = "timApp.timdb.docinfo.DocInfo" -DocumentType = "timApp.documentmodel.document.Document" -UserOrGroup = Union[UserType, UserGroupType] diff --git a/timApp/user/user.py b/timApp/user/user.py index 9514c7b93d..877eadb465 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -653,7 +653,7 @@ def is_current_user(self): return has_request_context() and get_current_user_id() == self.id @property - def pretty_full_name(self): + def pretty_full_name(self) -> str: """Returns the user's full name.""" if self.is_name_hidden: return f"User {self.id}" From 44f489aa94c2a3758924ac34a5de296bf477d5b9 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Sat, 29 Jul 2023 11:32:24 +0300 Subject: [PATCH 10/34] Clean up unused code --- timApp/tests/server/timroutetest.py | 29 +++++++++++++++++++++--- timApp/tim.py | 35 +---------------------------- timApp/timdb/sqa.py | 1 - timApp/user/user.py | 2 +- 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/timApp/tests/server/timroutetest.py b/timApp/tests/server/timroutetest.py index 7ade31485f..ecce15d3e4 100644 --- a/timApp/tests/server/timroutetest.py +++ b/timApp/tests/server/timroutetest.py @@ -107,6 +107,23 @@ def get_cookie_value(resp: Response, key: str) -> str | None: return None +def del_g(): + """ + Clean up global object. + + For some reason, the g object is not cleared when running browser test. + This helper method cleans up all the TIM-related attributes in the g object. + """ + if has_request_context() and app.config["TESTING"]: + g.pop("user", None) + g.pop("viewable", None) + g.pop("editable", None) + g.pop("teachable", None) + g.pop("manageable", None) + g.pop("see_answers", None) + g.pop("owned", None) + + class TimRouteTestBase(TimDbTest): """A base class for running tests for TIM routes.""" @@ -148,6 +165,7 @@ def setUp(self): # Instead, the client contex should be entered only in specific tests and explicitly self.client = self.client.__enter__() self.client.open("/") + del_g() @classmethod def add_language(cls, lang_name: str) -> Language: @@ -315,10 +333,10 @@ def clean_db_after_request(): try: yield finally: + del_g() if expire_session_after_request and has_app_context(): db.session.remove() # Reattach the user object to the session so that it can be tracked for changes - g.pop("user", None) with clean_db_after_request(): if headers is None: @@ -1024,7 +1042,13 @@ def create_translation( expect_status=expect_status, **kwargs, ) - return db.session.get(Translation, j["id"], options=[joinedload(Translation.docentry)]) if expect_status == 200 else None + return ( + db.session.get( + Translation, j["id"], options=[joinedload(Translation.docentry)] + ) + if expect_status == 200 + else None + ) def assert_content(self, element: HtmlElement, expected: list[str]): pars = get_content(element) @@ -1375,7 +1399,6 @@ def temp_config(self, settings: dict[str, Any]): class TimRouteTest(TimRouteTestBase): - def _init_client(self) -> FlaskClient: return timApp.tim.app.test_client() diff --git a/timApp/tim.py b/timApp/tim.py index 266948948c..9a2df31154 100755 --- a/timApp/tim.py +++ b/timApp/tim.py @@ -273,6 +273,7 @@ def install_sql_hook(): prev_exec_time = get_current_time() with app.app_context(): + @event.listens_for(db.engine, "before_execute") def receive_before_execute(conn, clauseelement, multiparams, params): nonlocal prev_exec_time @@ -358,34 +359,6 @@ def log_request(response): return response -@app.after_request -def close_db(response): - if hasattr(g, "timdb"): - g.timdb.close() - return response - - -@app.after_request -def del_g(response): - """For some reason, the g object is not cleared when running browser test, so we do it here.""" - if app.config["TESTING"]: - if hasattr(g, "user"): - del g.user - if hasattr(g, "viewable"): - del g.viewable - if hasattr(g, "editable"): - del g.editable - if hasattr(g, "teachable"): - del g.teachable - if hasattr(g, "manageable"): - del g.manageable - if hasattr(g, "see_answers"): - del g.see_answers - if hasattr(g, "owned"): - del g.owned - return response - - @app.after_request def after_request(resp: Response): token = generate_csrf() @@ -407,12 +380,6 @@ def after_request(resp: Response): return resp -@app.teardown_appcontext -def close_db_appcontext(_e): - if not app.config["TESTING"] and hasattr(g, "timdb"): - g.timdb.close() - - def init_app(): if app.config["PROFILE"]: app.wsgi_app = ProfilerMiddleware( diff --git a/timApp/timdb/sqa.py b/timApp/timdb/sqa.py index 07afdf89ea..d3e4309f01 100644 --- a/timApp/timdb/sqa.py +++ b/timApp/timdb/sqa.py @@ -30,7 +30,6 @@ db = SQLAlchemy(session_options=session_options, engine_options=engine_options) add_tim_types(db) -# TODO: Finish up adding Mapping annotations to everywhere # TODO: Replace db.Model with custom DeclarativeBase class that also specifies __tablename__ and custom types. # See https://docs.sqlalchemy.org/en/20/orm/declarative_mixins.html # TODO: Switch models to use dataclasses instead diff --git a/timApp/user/user.py b/timApp/user/user.py index 877eadb465..a8f120ee60 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -282,7 +282,7 @@ class User(db.Model, TimeStampMixin, SCIMEntity): real_name: Mapped[Optional[str]] """Real (full) name. This may be in the form "Lastname Firstname" or "Firstname Lastname".""" - _email: Mapped[str] = mapped_column("email", unique=True) + _email: Mapped[Optional[str]] = mapped_column("email", unique=True) """Email address.""" prefs: Mapped[Optional[str]] From c2f751445ce5b6dc582f565087d8da9a2f25e441 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Sat, 29 Jul 2023 16:00:58 +0300 Subject: [PATCH 11/34] Refactor to use SQLAlchemy classes directly instead of SQA-Flask proxy Instead of using references like db.relationship or db.Index, this commit refactors to use directly SQA's API for brevity and better typing. db.Model is replaced by TIM's own DbModel that inherits DeclarativeBase. This allows future-proof any behaviour changes to SQA or SQA-Flask --- timApp/answer/answer.py | 42 +++--- timApp/answer/answer_models.py | 33 ++--- timApp/auth/auth_models.py | 29 ++--- timApp/auth/oauth2/models.py | 43 ++++--- timApp/auth/session/model.py | 17 +-- timApp/celery_sqlalchemy_scheduler/models.py | 37 +++--- timApp/celery_sqlalchemy_scheduler/session.py | 4 +- timApp/document/docentry.py | 18 ++- timApp/document/translation/deepl.py | 26 ++-- timApp/document/translation/language.py | 5 +- timApp/document/translation/translation.py | 18 ++- timApp/document/translation/translator.py | 46 +++---- timApp/folder/folder.py | 15 +-- timApp/item/block.py | 55 ++++---- timApp/item/blockassociation.py | 11 +- timApp/item/blockrelevance.py | 14 +- timApp/item/tag.py | 14 +- timApp/item/taskblock.py | 13 +- timApp/lecture/askedjson.py | 14 +- timApp/lecture/askedquestion.py | 36 +++--- timApp/lecture/lecture.py | 31 ++--- timApp/lecture/lectureanswer.py | 24 ++-- timApp/lecture/lectureusers.py | 13 +- timApp/lecture/message.py | 17 ++- timApp/lecture/question.py | 9 +- timApp/lecture/questionactivity.py | 21 +-- timApp/lecture/routes.py | 26 ++-- timApp/lecture/runningquestion.py | 16 +-- timApp/lecture/showpoints.py | 15 ++- timApp/lecture/useractivity.py | 25 ++-- .../messagelist/messagelist_models.py | 102 +++++++-------- .../timMessage/internalmessage_models.py | 54 ++++---- timApp/note/usernote.py | 10 +- timApp/notification/notification.py | 14 +- timApp/notification/pending_notification.py | 18 ++- timApp/peerreview/peerreview.py | 16 +-- timApp/plugin/calendar/models.py | 58 +++------ timApp/plugin/pluginControl.py | 17 +-- timApp/plugin/plugintype.py | 15 +-- timApp/plugin/timtable/row_owner_info.py | 9 +- timApp/printing/printeddoc.py | 12 +- timApp/readmark/readparagraph.py | 19 ++- timApp/sisu/scimusergroup.py | 11 +- timApp/slide/slidestatus.py | 11 +- timApp/tests/server/test_lecture.py | 6 +- timApp/tim_app.py | 30 +++-- timApp/timdb/sqa.py | 17 ++- timApp/timdb/types.py | 27 ++-- timApp/user/consentchange.py | 11 +- timApp/user/hakaorganization.py | 5 +- timApp/user/newuser.py | 7 +- timApp/user/personaluniquecode.py | 13 +- timApp/user/user.py | 7 +- timApp/user/usercontact.py | 9 +- timApp/user/usergroup.py | 5 +- timApp/user/usergroupdoc.py | 13 +- timApp/user/usergroupmember.py | 21 ++- timApp/user/verification/verification.py | 16 +-- timApp/velp/annotation_model.py | 16 +-- timApp/velp/velp_models.py | 120 +++++++----------- tim_common/timjsonencoder.py | 5 +- 61 files changed, 654 insertions(+), 727 deletions(-) diff --git a/timApp/answer/answer.py b/timApp/answer/answer.py index 2a4278e217..7b22abab7b 100644 --- a/timApp/answer/answer.py +++ b/timApp/answer/answer.py @@ -1,13 +1,13 @@ import json from typing import Any, Optional, List, TYPE_CHECKING -from sqlalchemy import func -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import func, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.answer.answer_models import UserAnswer, AnswerUpload from timApp.plugin.taskid import TaskId from timApp.timdb.sqa import db, include_if_loaded -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.user.user import User @@ -15,36 +15,28 @@ from timApp.plugin.plugintype import PluginType -class AnswerSaver(db.Model): +class AnswerSaver(DbModel): """Holds information about who has saved an answer. For example, in teacher view, "Save teacher's fix" would store the teacher in this table. """ - __tablename__ = "answersaver" - - answer_id: Mapped[int] = mapped_column(db.ForeignKey("answer.id"), primary_key=True) - user_id: Mapped[int] = mapped_column( - db.ForeignKey("useraccount.id"), primary_key=True - ) + answer_id: Mapped[int] = mapped_column(ForeignKey("answer.id"), primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) -class Answer(db.Model): +class Answer(DbModel): """An answer to a task.""" - __tablename__ = "answer" - id: Mapped[int] = mapped_column(primary_key=True) """Answer identifier.""" task_id: Mapped[str] = mapped_column(index=True) """Task id to which this answer was posted. In the form "doc_id.name", for example "2.task1".""" - origin_doc_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) + origin_doc_id: Mapped[Optional[int]] = mapped_column(ForeignKey("block.id")) """The document in which the answer was saved""" - plugin_type_id: Mapped[Optional[int]] = mapped_column( - db.ForeignKey("plugintype.id") - ) + plugin_type_id: Mapped[Optional[int]] = mapped_column(ForeignKey("plugintype.id")) """Plugin type the answer was saved on""" content: Mapped[str] @@ -60,28 +52,26 @@ class Answer(db.Model): """Whether this answer is valid.""" last_points_modifier: Mapped[Optional[int]] = mapped_column( - db.ForeignKey("usergroup.id") + ForeignKey("usergroup.id") ) """The UserGroup who modified the points last. Null if the points have been given by the task automatically.""" - plugin_type: Mapped["PluginType"] = db.relationship(lazy="select") - uploads: Mapped[List["AnswerUpload"]] = db.relationship( + plugin_type: Mapped["PluginType"] = relationship(lazy="select") + uploads: Mapped[List["AnswerUpload"]] = relationship( back_populates="answer", lazy="dynamic" ) - users: Mapped[List["User"]] = db.relationship( + users: Mapped[List["User"]] = relationship( secondary=UserAnswer.__table__, back_populates="answers", lazy="dynamic" ) - users_all: Mapped[List["User"]] = db.relationship( + users_all: Mapped[List["User"]] = relationship( secondary=UserAnswer.__table__, back_populates="answers_alt", order_by="User.real_name", lazy="select", overlaps="users", ) - annotations: Mapped[List["Annotation"]] = db.relationship(back_populates="answer") - saver: Mapped["User"] = db.relationship( - lazy="select", secondary=AnswerSaver.__table__ - ) + annotations: Mapped[List["Annotation"]] = relationship(back_populates="answer") + saver: Mapped["User"] = relationship(lazy="select", secondary=AnswerSaver.__table__) @property def content_as_json(self) -> dict: diff --git a/timApp/answer/answer_models.py b/timApp/answer/answer_models.py index 1e518742ab..7b38efcaa8 100644 --- a/timApp/answer/answer_models.py +++ b/timApp/answer/answer_models.py @@ -1,52 +1,47 @@ from typing import TYPE_CHECKING, Optional -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import UniqueConstraint, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.item.block import Block from timApp.answer.answer import Answer -class AnswerTag(db.Model): +class AnswerTag(DbModel): """Tags for an Answer. TODO: Answer should be a Block and the tags would then come from the tag table. """ - __tablename__ = "answertag" - id: Mapped[int] = mapped_column(primary_key=True) - answer_id: Mapped[int] = mapped_column(db.ForeignKey("answer.id")) + answer_id: Mapped[int] = mapped_column(ForeignKey("answer.id")) tag: Mapped[str] -class AnswerUpload(db.Model): +class AnswerUpload(DbModel): """Associates uploaded files (Block with type BlockType.AnswerUpload) with Answers.""" - __tablename__ = "answerupload" - upload_block_id: Mapped[int] = mapped_column( - db.ForeignKey("block.id"), primary_key=True + ForeignKey("block.id"), primary_key=True ) - answer_id: Mapped[int] = mapped_column(db.ForeignKey("answer.id")) + answer_id: Mapped[Optional[int]] = mapped_column(ForeignKey("answer.id")) - block: Mapped["Block"] = db.relationship(back_populates="answerupload") - answer: Mapped["Answer"] = db.relationship(back_populates="uploads") + block: Mapped["Block"] = relationship(back_populates="answerupload") + answer: Mapped[Optional["Answer"]] = relationship(back_populates="uploads") def __init__(self, block, answer=None): self.block = block self.answer = answer -class UserAnswer(db.Model): +class UserAnswer(DbModel): """Associates Users with Answers.""" - __tablename__ = "useranswer" - id: Mapped[int] = mapped_column(primary_key=True) - answer_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("answer.id")) - user_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("useraccount.id")) + answer_id: Mapped[int] = mapped_column(ForeignKey("answer.id")) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) - __table_args__ = (db.UniqueConstraint("answer_id", "user_id"),) + __table_args__ = (UniqueConstraint("answer_id", "user_id"),) diff --git a/timApp/auth/auth_models.py b/timApp/auth/auth_models.py index d3d1fcfbdf..b10d97f4de 100644 --- a/timApp/auth/auth_models.py +++ b/timApp/auth/auth_models.py @@ -3,21 +3,22 @@ from datetime import datetime, timedelta from typing import TYPE_CHECKING, Optional, List -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.auth.accesstype import AccessType -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.item.distribute_rights import Right from timApp.item.block import Block from timApp.user.usergroup import UserGroup -from timApp.timdb.sqa import db, include_if_loaded +from timApp.timdb.sqa import include_if_loaded from timApp.util.utils import get_current_time -class AccessTypeModel(db.Model): +class AccessTypeModel(DbModel): """A kind of access that a UserGroup may have to a Block.""" __tablename__ = "accesstype" @@ -25,10 +26,10 @@ class AccessTypeModel(db.Model): id: Mapped[int] = mapped_column(primary_key=True) """Access type identifier.""" - name: Mapped[Optional[str]] + name: Mapped[str] """Access type name, such as 'view', 'edit', 'manage', etc.""" - accesses: Mapped[List["BlockAccess"]] = db.relationship(back_populates="atype") + accesses: Mapped[List["BlockAccess"]] = relationship(back_populates="atype") def __json__(self): return ["id", "name"] @@ -37,16 +38,14 @@ def to_enum(self): return AccessType(self.id) -class BlockAccess(db.Model): +class BlockAccess(DbModel): """A single permission. Relates a UserGroup with a Block along with an AccessType.""" - __tablename__ = "blockaccess" - - block_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + block_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) usergroup_id: Mapped[int] = mapped_column( - db.ForeignKey("usergroup.id"), primary_key=True + ForeignKey("usergroup.id"), primary_key=True ) - type: Mapped[int] = mapped_column(db.ForeignKey("accesstype.id"), primary_key=True) + type: Mapped[int] = mapped_column(ForeignKey("accesstype.id"), primary_key=True) accessible_from: Mapped[Optional[datetime_tz]] accessible_to: Mapped[Optional[datetime_tz]] duration: Mapped[Optional[timedelta]] @@ -54,9 +53,9 @@ class BlockAccess(db.Model): duration_to: Mapped[Optional[datetime_tz]] require_confirm: Mapped[Optional[bool]] - block: Mapped["Block"] = db.relationship(back_populates="accesses") - usergroup: Mapped["UserGroup"] = db.relationship(back_populates="accesses") - atype: Mapped["AccessTypeModel"] = db.relationship(back_populates="accesses") + block: Mapped["Block"] = relationship(back_populates="accesses") + usergroup: Mapped["UserGroup"] = relationship(back_populates="accesses") + atype: Mapped["AccessTypeModel"] = relationship(back_populates="accesses") @property def duration_now(self): diff --git a/timApp/auth/oauth2/models.py b/timApp/auth/oauth2/models.py index 925d894364..ae48d6bb2d 100644 --- a/timApp/auth/oauth2/models.py +++ b/timApp/auth/oauth2/models.py @@ -10,9 +10,10 @@ TokenMixin, AuthorizationCodeMixin, ) -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.user.user import User @@ -108,18 +109,18 @@ def check_grant_type(self, grant_type: str) -> bool: return grant_type in self.grant_types -class OAuth2Token(db.Model, TokenMixin): +class OAuth2Token(DbModel, TokenMixin): __tablename__ = "oauth2_token" id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) - user: Mapped["User"] = db.relationship() - - client_id: Mapped[Optional[str]] = mapped_column(db.String(48)) - token_type: Mapped[Optional[str]] = mapped_column(db.String(40)) - access_token: Mapped[str] = mapped_column(db.String(255), unique=True) - refresh_token: Mapped[Optional[str]] = mapped_column(db.String(255), index=True) - scope: Mapped[str] = mapped_column(default="") + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) + user: Mapped["User"] = relationship() + + client_id: Mapped[Optional[str]] = mapped_column(String(48)) + token_type: Mapped[Optional[str]] = mapped_column(String(40)) + access_token: Mapped[str] = mapped_column(String(255), unique=True) + refresh_token: Mapped[Optional[str]] = mapped_column(String(255), index=True) + scope: Mapped[Optional[str]] = mapped_column(default="") issued_at: Mapped[int] = mapped_column(default=lambda: int(time.time())) access_token_revoked_at: Mapped[int] = mapped_column(default=0) refresh_token_revoked_at: Mapped[int] = mapped_column(default=0) @@ -145,23 +146,23 @@ def is_expired(self): return expires_at < time.time() -class OAuth2AuthorizationCode(db.Model, AuthorizationCodeMixin): +class OAuth2AuthorizationCode(DbModel, AuthorizationCodeMixin): __tablename__ = "oauth2_auth_code" id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) - user: Mapped["User"] = db.relationship() - - code: Mapped[str] = mapped_column(db.String(120), unique=True) - client_id: Mapped[str] = mapped_column(db.String(48)) - redirect_uri: Mapped[str] = mapped_column(default="") - response_type: Mapped[str] = mapped_column(default="") - scope: Mapped[str] = mapped_column(default="") + user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("useraccount.id")) + user: Mapped["User"] = relationship() + + code: Mapped[str] = mapped_column(String(120), unique=True) + client_id: Mapped[Optional[str]] = mapped_column(String(48)) + redirect_uri: Mapped[Optional[str]] = mapped_column(default="") + response_type: Mapped[Optional[str]] = mapped_column(default="") + scope: Mapped[Optional[str]] = mapped_column(default="") nonce: Mapped[Optional[str]] auth_time: Mapped[int] = mapped_column(default=lambda: int(time.time())) code_challenge: Mapped[Optional[str]] - code_challenge_method: Mapped[Optional[str]] = mapped_column(db.String(48)) + code_challenge_method: Mapped[Optional[str]] = mapped_column(String(48)) def is_expired(self): return self.auth_time + 300 < time.time() diff --git a/timApp/auth/session/model.py b/timApp/auth/session/model.py index 38cd30d81f..70b9eb0ad4 100755 --- a/timApp/auth/session/model.py +++ b/timApp/auth/session/model.py @@ -4,17 +4,18 @@ from datetime import datetime from typing import Optional, TYPE_CHECKING -from sqlalchemy.ext.hybrid import hybrid_property # type: ignore -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import ForeignKey +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel from timApp.util.utils import get_current_time if TYPE_CHECKING: from timApp.user.user import User -class UserSession(db.Model): +class UserSession(DbModel): """ User session. A session is given to the user when they log in. @@ -24,11 +25,7 @@ class UserSession(db.Model): :attr:`timApp.defaultconfig.SESSIONS_ENABLE` is set. """ - __tablename__ = "usersession" - - user_id: Mapped[int] = mapped_column( - db.ForeignKey("useraccount.id"), primary_key=True - ) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) """ User ID of the user who owns the session. """ @@ -54,7 +51,7 @@ class UserSession(db.Model): May include user agent and any other information about login state. """ - user: Mapped["User"] = db.relationship("User", back_populates="sessions") + user: Mapped["User"] = relationship("User", back_populates="sessions") """ User that owns the session. Relationship to :attr:`user_id`. """ diff --git a/timApp/celery_sqlalchemy_scheduler/models.py b/timApp/celery_sqlalchemy_scheduler/models.py index dcc1f13073..aba5ffb049 100644 --- a/timApp/celery_sqlalchemy_scheduler/models.py +++ b/timApp/celery_sqlalchemy_scheduler/models.py @@ -39,7 +39,6 @@ def update(self, **kw): class IntervalSchedule(ModelBase, ModelMixin): __tablename__ = "celery_interval_schedule" __table_args__ = {"sqlite_autoincrement": True} - DAYS = "days" HOURS = "hours" @@ -50,7 +49,7 @@ class IntervalSchedule(ModelBase, ModelMixin): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) every: Mapped[int] - period: Mapped[str] = mapped_column(sa.String(24)) + period: Mapped[Optional[str]] = mapped_column(sa.String(24)) def __repr__(self): if self.every == 1: @@ -89,15 +88,14 @@ def period_singular(self): class CrontabSchedule(ModelBase, ModelMixin): __tablename__ = "celery_crontab_schedule" __table_args__ = {"sqlite_autoincrement": True} - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - minute: Mapped[str] = mapped_column(sa.String(60 * 4), default="*") - hour: Mapped[str] = mapped_column(sa.String(24 * 4), default="*") - day_of_week: Mapped[str] = mapped_column(sa.String(64), default="*") - day_of_month: Mapped[str] = mapped_column(sa.String(31 * 4), default="*") - month_of_year: Mapped[str] = mapped_column(sa.String(64), default="*") - timezone: Mapped[str] = mapped_column(sa.String(64), default="UTC") + minute: Mapped[Optional[str]] = mapped_column(sa.String(60 * 4), default="*") + hour: Mapped[Optional[str]] = mapped_column(sa.String(24 * 4), default="*") + day_of_week: Mapped[Optional[str]] = mapped_column(sa.String(64), default="*") + day_of_month: Mapped[Optional[str]] = mapped_column(sa.String(31 * 4), default="*") + month_of_year: Mapped[Optional[str]] = mapped_column(sa.String(64), default="*") + timezone: Mapped[Optional[str]] = mapped_column(sa.String(64), default="UTC") def __repr__(self): return "{} {} {} {} {} (m/h/d/dM/MY) {}".format( @@ -182,7 +180,6 @@ class PeriodicTaskChanged(ModelBase, ModelMixin): """Helper table for tracking updates to periodic tasks.""" __tablename__ = "celery_periodic_task_changed" - id: Mapped[int] = mapped_column(primary_key=True) last_update: Mapped[datetime_tz] = mapped_column(default=dt.datetime.now) @@ -235,11 +232,11 @@ class PeriodicTask(ModelBase, ModelMixin): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) block_id: Mapped[Optional[int]] = mapped_column(sa.ForeignKey("block.id")) - block: Mapped[Block] = relationship(Block) + block: Mapped[Optional[Block]] = relationship(Block) # name - name: Mapped[str] = mapped_column(sa.String(255), unique=True) + name: Mapped[Optional[str]] = mapped_column(sa.String(255), unique=True) # task name - task: Mapped[str] = mapped_column(sa.String(255)) + task: Mapped[Optional[str]] = mapped_column(sa.String(255)) # not use ForeignKey interval_id: Mapped[Optional[int]] @@ -257,8 +254,8 @@ class PeriodicTask(ModelBase, ModelMixin): primaryjoin="foreign(PeriodicTask.solar_id) == remote(SolarSchedule.id)", ) - args: Mapped[str] = mapped_column(default="[]") - kwargs: Mapped[str] = mapped_column(default="{}") + args: Mapped[Optional[str]] = mapped_column(default="[]") + kwargs: Mapped[Optional[str]] = mapped_column(default="{}") # queue for celery queue: Mapped[Optional[str]] = mapped_column(sa.String(255)) # exchange for celery @@ -269,14 +266,16 @@ class PeriodicTask(ModelBase, ModelMixin): expires: Mapped[Optional[datetime_tz]] # 只执行一次 - one_off: Mapped[bool] = mapped_column(default=False) + one_off: Mapped[Optional[bool]] = mapped_column(default=False) start_time: Mapped[Optional[datetime_tz]] - enabled: Mapped[bool] = mapped_column(default=True) + enabled: Mapped[Optional[bool]] = mapped_column(default=True) last_run_at: Mapped[Optional[datetime_tz]] total_run_count: Mapped[int] = mapped_column(default=0) # 修改时间 - date_changed: Mapped[datetime_tz] = mapped_column(default=func.now(), onupdate=func.now()) - description: Mapped[str] = mapped_column(default="") + date_changed: Mapped[Optional[datetime_tz]] = mapped_column( + default=func.now(), onupdate=func.now() + ) + description: Mapped[Optional[str]] = mapped_column(default="") no_changes = False diff --git a/timApp/celery_sqlalchemy_scheduler/session.py b/timApp/celery_sqlalchemy_scheduler/session.py index 0b5d03fafa..7009b156a8 100644 --- a/timApp/celery_sqlalchemy_scheduler/session.py +++ b/timApp/celery_sqlalchemy_scheduler/session.py @@ -7,9 +7,9 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import NullPool -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel -ModelBase = db.Model +ModelBase = DbModel @contextmanager diff --git a/timApp/document/docentry.py b/timApp/document/docentry.py index 81188ff802..dac4f76f47 100644 --- a/timApp/document/docentry.py +++ b/timApp/document/docentry.py @@ -2,8 +2,8 @@ from typing import TYPE_CHECKING, Any, List -from sqlalchemy import select -from sqlalchemy.orm import foreign, mapped_column, Mapped +from sqlalchemy import select, ForeignKey, Index +from sqlalchemy.orm import foreign, mapped_column, Mapped, relationship from timApp.document.docinfo import DocInfo from timApp.document.document import Document @@ -13,6 +13,7 @@ from timApp.item.block import insert_block from timApp.timdb.exceptions import ItemAlreadyExistsException from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup, get_admin_group_id from timApp.util.utils import split_location @@ -20,7 +21,7 @@ from timApp.user.user import User -class DocEntry(db.Model, DocInfo): +class DocEntry(DbModel, DocInfo): """Represents a TIM document in the directory hierarchy. A document can have several aliases, which is why the primary key is "name" column and not "id". @@ -28,24 +29,21 @@ class DocEntry(db.Model, DocInfo): Most of the time you should use DocInfo class instead of this. """ - __tablename__ = "docentry" - - name: Mapped[str] = mapped_column(primary_key=True) """Full path of the document. TODO: Improve the name. """ - id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + id: Mapped[int] = mapped_column(ForeignKey("block.id")) """Document identifier.""" public: Mapped[bool] = mapped_column(default=True) """Whether the document is visible in directory listing.""" - _block: Mapped["Block"] = db.relationship(back_populates="docentries", lazy="joined") + _block: Mapped["Block"] = relationship(back_populates="docentries", lazy="joined") - trs: Mapped[List[Translation]] = db.relationship( + trs: Mapped[List[Translation]] = relationship( primaryjoin=id == foreign(Translation.src_docid), back_populates="docentry", # When a DocEntry object is deleted, we don't want to touch the translation objects at all. @@ -55,7 +53,7 @@ class DocEntry(db.Model, DocInfo): passive_deletes="all", ) - __table_args__ = (db.Index("docentry_id_idx", "id"),) + __table_args__ = (Index("docentry_id_idx", "id"),) @property def tr(self) -> Translation | None: diff --git a/timApp/document/translation/deepl.py b/timApp/document/translation/deepl.py index 6b8cf7d476..56a08028a0 100644 --- a/timApp/document/translation/deepl.py +++ b/timApp/document/translation/deepl.py @@ -15,11 +15,13 @@ __license__ = "MIT" __date__ = "25.4.2022" +from typing import Optional + import langcodes from requests import post, Response from requests.exceptions import JSONDecodeError from sqlalchemy import select -from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import Mapped from timApp.document.translation.language import Language from timApp.document.translation.translationparser import TranslateApproval, NoTranslate @@ -53,14 +55,14 @@ def __init__(self, values: dict): self.ignore_tag = values["ignore_tag"] self.service_url = values["service_url"] - # TODO Would be better as nullable=False, but that prevents creating + # TODO Would be better as non-optional, but that prevents creating # non-DeeplTranslationService -subclasses of TranslationService. - service_url = mapped_column(db.Text) + service_url: Mapped[Optional[str]] """The url base for the API calls.""" - # TODO Would be better as nullable=False, but that prevents creating + # TODO Would be better as non-optional, but that prevents creating # non-DeeplTranslationService -subclasses of TranslationService. - ignore_tag = mapped_column(db.Text) + ignore_tag: Mapped[Optional[str]] """The XML-tag name to use for ignoring pieces of text when XML-handling is used. Should be chosen to be some uncommon string not found in many texts. """ @@ -83,10 +85,16 @@ def register(self, user_group: UserGroup) -> None: :raises RouteException: If more than one key is found from user. """ # One user group should match one service per one key. - api_key = db.session.execute(select(TranslationServiceKey).filter( - TranslationServiceKey.service_id == self.id, - TranslationServiceKey.group_id == user_group.id, - )).scalars().all() + api_key = ( + db.session.execute( + select(TranslationServiceKey).filter( + TranslationServiceKey.service_id == self.id, + TranslationServiceKey.group_id == user_group.id, + ) + ) + .scalars() + .all() + ) if len(api_key) == 0: raise NotExist( "Please add a DeepL API key that corresponds the chosen plan into your account" diff --git a/timApp/document/translation/language.py b/timApp/document/translation/language.py index c2dbab50f7..fc3a94fea8 100644 --- a/timApp/document/translation/language.py +++ b/timApp/document/translation/language.py @@ -20,9 +20,10 @@ from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel -class Language(db.Model): +class Language(DbModel): """Represents a standardized language code used for example with translation documents. @@ -30,8 +31,6 @@ class Language(db.Model): instances! """ - __tablename__ = "language" - lang_code: Mapped[str] = mapped_column(primary_key=True) """Standardized code of the language.""" diff --git a/timApp/document/translation/translation.py b/timApp/document/translation/translation.py index d922b83ae0..8dada35d15 100644 --- a/timApp/document/translation/translation.py +++ b/timApp/document/translation/translation.py @@ -1,17 +1,18 @@ from typing import TYPE_CHECKING -from sqlalchemy import UniqueConstraint -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import UniqueConstraint, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.document.docinfo import DocInfo from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.item.block import Block from timApp.document.docentry import DocEntry -class Translation(db.Model, DocInfo): +class Translation(DbModel, DocInfo): """A translated document. Translation objects may be created in two scenarios: @@ -21,19 +22,16 @@ class Translation(db.Model, DocInfo): """ - __tablename__ = "translation" - - - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) - src_docid: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) + src_docid: Mapped[int] = mapped_column(ForeignKey("block.id")) lang_id: Mapped[str] __table_args__ = (UniqueConstraint("src_docid", "lang_id", name="translation_uc"),) - _block: Mapped["Block"] = db.relationship( + _block: Mapped["Block"] = relationship( "Block", back_populates="translation", foreign_keys=[doc_id] ) - docentry: Mapped["DocEntry"] = db.relationship( + docentry: Mapped["DocEntry"] = relationship( back_populates="trs", primaryjoin="foreign(Translation.src_docid) == DocEntry.id", ) diff --git a/timApp/document/translation/translator.py b/timApp/document/translation/translator.py index 7beca98475..30d0f25a5e 100644 --- a/timApp/document/translation/translator.py +++ b/timApp/document/translation/translator.py @@ -21,8 +21,8 @@ from dataclasses import dataclass import pypandoc -from sqlalchemy import select -from sqlalchemy.orm import with_polymorphic, mapped_column, Mapped +from sqlalchemy import select, ForeignKey +from sqlalchemy.orm import with_polymorphic, mapped_column, Mapped, relationship from timApp.document.docparagraph import DocParagraph from timApp.document.translation.language import Language @@ -34,6 +34,7 @@ Translate, ) from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup from timApp.util import logger from timApp.util.flask.requesthelper import RouteException @@ -69,14 +70,11 @@ def __getitem__(self, item: str) -> list[Language]: return self.value[item] -class TranslationService(db.Model): +class TranslationService(DbModel): """Represents the information and methods that must be available from all possible machine translators. """ - __tablename__ = "translationservice" - - id: Mapped[int] = mapped_column(primary_key=True) """Translation service identifier.""" @@ -170,17 +168,14 @@ def get_languages(self, source_langs: bool) -> list[Language]: # Polymorphism allows querying multiple objects by their class e.g. # `TranslationService.query`. - __mapper_args__ = {"polymorphic_on": service_name} + __mapper_args__ = {"polymorphic_on": "service_name"} -class TranslationServiceKey(db.Model): +class TranslationServiceKey(DbModel): """Represents an API-key (or any string value) that is needed for using a machine translator and that one or more users are in possession of. """ - __tablename__ = "translationservicekey" - - id: Mapped[int] = mapped_column(primary_key=True) """Key identifier.""" @@ -188,12 +183,12 @@ class TranslationServiceKey(db.Model): api_key: Mapped[str] """The key needed for using related service.""" - group_id: Mapped[int] = mapped_column(db.ForeignKey("usergroup.id")) - group: Mapped[UserGroup] = db.relationship() + group_id: Mapped[int] = mapped_column(ForeignKey("usergroup.id")) + group: Mapped[UserGroup] = relationship() """The group that can use this key.""" - service_id: Mapped[int] = mapped_column(db.ForeignKey("translationservice.id")) - service: Mapped[TranslationService] = db.relationship() + service_id: Mapped[int] = mapped_column(ForeignKey("translationservice.id")) + service: Mapped[TranslationService] = relationship() """The service that this key is used in.""" @staticmethod @@ -207,9 +202,11 @@ def get_by_user_group( :return: The first matching TranslationServiceKey instance, if one is found. """ - return db.session.execute(select(TranslationServiceKey).filter( - TranslationServiceKey.group_id == user_group - )).first() + return db.session.execute( + select(TranslationServiceKey).filter( + TranslationServiceKey.group_id == user_group + ) + ).first() def to_json(self) -> dict: """ @@ -277,10 +274,15 @@ def __init__( the user sets to their account). """ - translator = db.session.execute( - select(with_polymorphic(TranslationService, "*")) - .filter(TranslationService.service_name == translator_code) - ).scalars().one() + translator = ( + db.session.execute( + select(with_polymorphic(TranslationService, "*")).filter( + TranslationService.service_name == translator_code + ) + ) + .scalars() + .one() + ) if user_group is not None and isinstance( translator, RegisteredTranslationService diff --git a/timApp/folder/folder.py b/timApp/folder/folder.py index 052f9dda96..290bfe43ad 100644 --- a/timApp/folder/folder.py +++ b/timApp/folder/folder.py @@ -2,8 +2,8 @@ from typing import Iterable, Any, TYPE_CHECKING -from sqlalchemy import true, and_, select, delete -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import true, and_, select, delete, UniqueConstraint, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.auth.auth_models import BlockAccess from timApp.document.docentry import DocEntry, get_documents @@ -14,6 +14,7 @@ from timApp.item.item import Item from timApp.timdb.exceptions import ItemAlreadyExistsException from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup from timApp.util.utils import split_location, join_location, relative_location @@ -23,12 +24,10 @@ ROOT_FOLDER_ID = -1 -class Folder(db.Model, Item): +class Folder(DbModel, Item): """Represents a folder in the directory hierarchy.""" - __tablename__ = "folder" - - id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) """Folder identifier.""" name: Mapped[str] @@ -37,9 +36,9 @@ class Folder(db.Model, Item): location: Mapped[str] """Folder location (first parts of the path).""" - __table_args__ = (db.UniqueConstraint("name", "location", name="folder_uc"),) + __table_args__ = (UniqueConstraint("name", "location", name="folder_uc"),) - _block: Mapped[Block] = db.relationship(back_populates="folder", lazy="joined") + _block: Mapped[Block] = relationship(back_populates="folder", lazy="joined") @staticmethod def get_root() -> Folder: diff --git a/timApp/item/block.py b/timApp/item/block.py index 455013882d..06eeec05db 100644 --- a/timApp/item/block.py +++ b/timApp/item/block.py @@ -4,13 +4,19 @@ from typing import TYPE_CHECKING, Optional, List, Dict, Tuple from sqlalchemy import func -from sqlalchemy.orm import mapped_column, Mapped, attribute_keyed_dict, DynamicMapped +from sqlalchemy.orm import ( + mapped_column, + Mapped, + attribute_keyed_dict, + DynamicMapped, + relationship, +) from timApp.auth.accesstype import AccessType from timApp.auth.auth_models import BlockAccess from timApp.item.blockassociation import BlockAssociation from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel from timApp.user.usergroup import UserGroup from timApp.user.usergroupdoc import UserGroupDoc from timApp.util.utils import get_current_time @@ -24,14 +30,15 @@ from timApp.notification.notification import Notification from timApp.item.blockrelevance import BlockRelevance from timApp.messaging.messagelist.messagelist_models import MessageListModel - from timApp.messaging.timMessage.internalmessage_models import InternalMessage, InternalMessageDisplay + from timApp.messaging.timMessage.internalmessage_models import ( + InternalMessage, + InternalMessageDisplay, + ) -class Block(db.Model): +class Block(DbModel): """The "base class" for all database objects that are part of the permission system.""" - __tablename__ = "block" - id: Mapped[int] = mapped_column(primary_key=True) """A unique identifier for the Block.""" @@ -52,48 +59,46 @@ class Block(db.Model): modified: Mapped[Optional[datetime_tz]] = mapped_column(default=func.now()) """When this Block was last modified.""" - docentries: Mapped[List["DocEntry"]] = db.relationship(back_populates="_block") - folder: Mapped[Optional[Folder]] = db.relationship( - back_populates="_block" + docentries: Mapped[List["DocEntry"]] = relationship(back_populates="_block") + folder: Mapped[Optional[Folder]] = relationship(back_populates="_block") + translation: Mapped[Optional["Translation"]] = relationship( + "Translation", back_populates="_block", foreign_keys="Translation.doc_id" ) - translation: Mapped[Optional["Translation"]] = db.relationship( - "Translation", - back_populates="_block", - foreign_keys="Translation.doc_id" - ) - answerupload: DynamicMapped[Optional["AnswerUpload"]] = db.relationship( + answerupload: DynamicMapped[Optional["AnswerUpload"]] = relationship( back_populates="block", lazy="dynamic" ) - accesses: Mapped[Dict[Tuple[int, int], "BlockAccess"]] = db.relationship( + accesses: Mapped[Dict[Tuple[int, int], "BlockAccess"]] = relationship( back_populates="block", lazy="selectin", cascade="all, delete-orphan", collection_class=attribute_keyed_dict("block_collection_key"), ) - tags: Mapped[List["Tag"]] = db.relationship("Tag", back_populates="block", lazy="select") - children: Mapped[List["Block"]] = db.relationship( + tags: Mapped[List["Tag"]] = relationship( + "Tag", back_populates="block", lazy="select" + ) + children: Mapped[List["Block"]] = relationship( secondary=BlockAssociation.__table__, primaryjoin=id == BlockAssociation.__table__.c.parent, secondaryjoin=id == BlockAssociation.__table__.c.child, lazy="select", ) - parents: Mapped[List["Block"]] = db.relationship( + parents: Mapped[List["Block"]] = relationship( secondary=BlockAssociation.__table__, primaryjoin=id == BlockAssociation.__table__.c.child, secondaryjoin=id == BlockAssociation.__table__.c.parent, lazy="select", overlaps="children", ) - notifications: DynamicMapped["Notification"] = db.relationship( + notifications: DynamicMapped["Notification"] = relationship( back_populates="block", lazy="dynamic" ) - relevance: Mapped[Optional["BlockRelevance"]] = db.relationship( + relevance: Mapped[Optional["BlockRelevance"]] = relationship( back_populates="_block" ) # If this Block corresponds to a group's manage document, indicates the group being managed. - managed_usergroup: Mapped[Optional[UserGroup]] = db.relationship( + managed_usergroup: Mapped[Optional[UserGroup]] = relationship( secondary=UserGroupDoc.__table__, lazy="select", overlaps="admin_doc", @@ -101,14 +106,14 @@ class Block(db.Model): # If this Block corresponds to a message list's manage document, indicates the message list # being managed. - managed_messagelist: Mapped[Optional["MessageListModel"]] = db.relationship( + managed_messagelist: Mapped[Optional["MessageListModel"]] = relationship( back_populates="block", lazy="select" ) - internalmessage: Mapped[Optional["InternalMessage"]] = db.relationship( + internalmessage: Mapped[Optional["InternalMessage"]] = relationship( back_populates="block" ) - internalmessage_display: Mapped[Optional["InternalMessageDisplay"]] = db.relationship( + internalmessage_display: Mapped[Optional["InternalMessageDisplay"]] = relationship( back_populates="display_block" ) diff --git a/timApp/item/blockassociation.py b/timApp/item/blockassociation.py index 33a550e89d..580c3aad90 100644 --- a/timApp/item/blockassociation.py +++ b/timApp/item/blockassociation.py @@ -1,15 +1,14 @@ +from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel -class BlockAssociation(db.Model): +class BlockAssociation(DbModel): """Associates blocks with other blocks. Currently only used for associating uploaded files with documents.""" - __tablename__ = "blockassociation" - - parent: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + parent: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) """The parent Block.""" - child: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + child: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) """The child Block.""" diff --git a/timApp/item/blockrelevance.py b/timApp/item/blockrelevance.py index 63f7c5fcca..60124d650c 100644 --- a/timApp/item/blockrelevance.py +++ b/timApp/item/blockrelevance.py @@ -1,18 +1,18 @@ from typing import TYPE_CHECKING -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.item.block import Block -class BlockRelevance(db.Model): - """A relevance value of a block (used in search).""" - __tablename__ = "blockrelevance" +class BlockRelevance(DbModel): + """A relevance value of a block (used in search).""" - block_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + block_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) relevance: Mapped[int] - _block: Mapped["Block"] = db.relationship(back_populates="relevance") + _block: Mapped["Block"] = relationship(back_populates="relevance") diff --git a/timApp/item/tag.py b/timApp/item/tag.py index 2048f7a3cc..96306d32fb 100644 --- a/timApp/item/tag.py +++ b/timApp/item/tag.py @@ -1,10 +1,10 @@ from enum import Enum, unique from typing import Optional, TYPE_CHECKING -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.item.block import Block @@ -24,17 +24,15 @@ class TagType(Enum): """The Tag is the name for a subject.""" -class Tag(db.Model): +class Tag(DbModel): """A tag with associated document id, tag name, type and expiration date.""" - __tablename__ = "tag" - - block_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + block_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) name: Mapped[str] = mapped_column(primary_key=True) type: Mapped[TagType] expires: Mapped[Optional[datetime_tz]] - block: Mapped["Block"] = db.relationship(back_populates="tags") + block: Mapped["Block"] = relationship(back_populates="tags") def __json__(self): return ["block_id", "name", "type", "expires"] diff --git a/timApp/item/taskblock.py b/timApp/item/taskblock.py index b2f7f4fd86..87c586c685 100644 --- a/timApp/item/taskblock.py +++ b/timApp/item/taskblock.py @@ -1,20 +1,19 @@ from __future__ import annotations -from sqlalchemy import select -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import select, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.item.block import Block, BlockType, insert_block from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup -class TaskBlock(db.Model): - __tablename__ = "taskblock" - - id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) +class TaskBlock(DbModel): + id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) task_id: Mapped[str] = mapped_column(primary_key=True) - block: Mapped[Block] = db.relationship(lazy="select") + block: Mapped[Block] = relationship(lazy="select") @staticmethod def get_by_task(task_id: str) -> TaskBlock | None: diff --git a/timApp/lecture/askedjson.py b/timApp/lecture/askedjson.py index c47b50ae14..4de1aeba86 100644 --- a/timApp/lecture/askedjson.py +++ b/timApp/lecture/askedjson.py @@ -1,22 +1,24 @@ import json from copy import deepcopy -from typing import Any +from typing import Any, TYPE_CHECKING from sqlalchemy import select -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel +if TYPE_CHECKING: + from timApp.lecture.askedquestion import AskedQuestion -class AskedJson(db.Model): - __tablename__ = "askedjson" +class AskedJson(DbModel): asked_json_id: Mapped[int] = mapped_column(primary_key=True) json: Mapped[str] hash: Mapped[str] - asked_questions = db.relationship( - "AskedQuestion", back_populates="asked_json", lazy="selectin" + asked_questions: Mapped["AskedQuestion"] = relationship( + back_populates="asked_json", lazy="selectin" ) def to_json(self, hide_points=False): diff --git a/timApp/lecture/askedquestion.py b/timApp/lecture/askedquestion.py index 0ab01279cd..1e79733aff 100644 --- a/timApp/lecture/askedquestion.py +++ b/timApp/lecture/askedquestion.py @@ -3,13 +3,13 @@ from datetime import timedelta, datetime from typing import Optional, TYPE_CHECKING, List -from sqlalchemy import func, select -from sqlalchemy.orm import mapped_column, Mapped, DynamicMapped +from sqlalchemy import func, select, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, DynamicMapped, relationship from timApp.lecture.question_utils import qst_rand_array, qst_filter_markup_points from timApp.lecture.questionactivity import QuestionActivityKind, QuestionActivity from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel from timApp.util.utils import get_current_time if TYPE_CHECKING: @@ -17,42 +17,40 @@ from timApp.lecture.askedjson import AskedJson from timApp.lecture.lecture import Lecture from timApp.lecture.lectureanswer import LectureAnswer - from timApp.lecture.runningquestion import Runningquestion - from timApp.lecture.showpoints import Showpoints + from timApp.lecture.runningquestion import RunningQuestion + from timApp.lecture.showpoints import ShowPoints -class AskedQuestion(db.Model): - __tablename__ = "askedquestion" - +class AskedQuestion(DbModel): asked_id: Mapped[int] = mapped_column(primary_key=True) - lecture_id: Mapped[int] = mapped_column(db.ForeignKey("lecture.lecture_id")) - doc_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) + lecture_id: Mapped[int] = mapped_column(ForeignKey("lecture.lecture_id")) + doc_id: Mapped[Optional[int]] = mapped_column(ForeignKey("block.id")) par_id: Mapped[Optional[str]] asked_time: Mapped[datetime_tz] points: Mapped[Optional[str]] - asked_json_id: Mapped[int] = mapped_column(db.ForeignKey("askedjson.asked_json_id")) + asked_json_id: Mapped[int] = mapped_column(ForeignKey("askedjson.asked_json_id")) expl: Mapped[Optional[str]] - asked_json: Mapped["AskedJson"] = db.relationship( + asked_json: Mapped["AskedJson"] = relationship( back_populates="asked_questions", lazy="selectin" ) - lecture: Mapped["Lecture"] = db.relationship( + lecture: Mapped["Lecture"] = relationship( back_populates="asked_questions", lazy="selectin" ) - answers: DynamicMapped["LectureAnswer"] = db.relationship( + answers: DynamicMapped["LectureAnswer"] = relationship( back_populates="asked_question", lazy="dynamic" ) - answers_all: Mapped[List["LectureAnswer"]] = db.relationship( + answers_all: Mapped[List["LectureAnswer"]] = relationship( back_populates="asked_question", overlaps="answers" ) - running_question: Mapped[Optional["Runningquestion"]] = db.relationship( + running_question: Mapped[Optional["RunningQuestion"]] = relationship( back_populates="asked_question", lazy="select" ) - questionactivity: DynamicMapped["QuestionActivity"] = db.relationship( + questionactivity: DynamicMapped["QuestionActivity"] = relationship( back_populates="asked_question", lazy="dynamic" ) - showpoints: Mapped[Optional["Showpoints"]] = db.relationship( - "Showpoints", back_populates="asked_question", lazy="select" + showpoints: Mapped[Optional["ShowPoints"]] = relationship( + back_populates="asked_question", lazy="select" ) @property diff --git a/timApp/lecture/lecture.py b/timApp/lecture/lecture.py index 5ef87c97c0..c51dbc1289 100644 --- a/timApp/lecture/lecture.py +++ b/timApp/lecture/lecture.py @@ -2,51 +2,52 @@ from datetime import datetime, timezone from typing import Optional, TYPE_CHECKING, List -from sqlalchemy import select, func -from sqlalchemy.orm import mapped_column, Mapped, DynamicMapped +from sqlalchemy import select, func, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, DynamicMapped, relationship from timApp.lecture.lectureusers import LectureUsers from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel from timApp.util.utils import get_current_time if TYPE_CHECKING: from timApp.user.user import User from timApp.lecture.askedquestion import AskedQuestion from timApp.lecture.message import Message - from timApp.lecture.runningquestion import Runningquestion - from timApp.lecture.useractivity import Useractivity + from timApp.lecture.runningquestion import RunningQuestion + from timApp.lecture.useractivity import UserActivity -class Lecture(db.Model): - __tablename__ = "lecture" +class Lecture(DbModel): lecture_id: Mapped[int] = mapped_column(primary_key=True) lecture_code: Mapped[Optional[str]] - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) - lecturer: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id")) + lecturer: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) start_time: Mapped[datetime_tz] end_time: Mapped[Optional[datetime_tz]] password: Mapped[Optional[str]] options: Mapped[Optional[str]] - users: DynamicMapped["User"] = db.relationship( + users: DynamicMapped["User"] = relationship( secondary=LectureUsers.__table__, back_populates="lectures", lazy="dynamic", ) - asked_questions: DynamicMapped["AskedQuestion"] = db.relationship( + asked_questions: DynamicMapped["AskedQuestion"] = relationship( back_populates="lecture", lazy="dynamic", ) - messages: DynamicMapped["Message"] = db.relationship(back_populates="lecture", lazy="dynamic") - running_questions: Mapped[List["Runningquestion"]] = db.relationship( + messages: DynamicMapped["Message"] = relationship( + back_populates="lecture", lazy="dynamic" + ) + running_questions: Mapped[List["RunningQuestion"]] = relationship( back_populates="lecture", lazy="select", ) - useractivity: Mapped[List["Useractivity"]] = db.relationship( + useractivity: Mapped[List["UserActivity"]] = relationship( back_populates="lecture", lazy="select" ) - owner: Mapped["User"] = db.relationship(back_populates="owned_lectures") + owner: Mapped["User"] = relationship(back_populates="owned_lectures") @staticmethod def find_by_id(lecture_id: int) -> Optional["Lecture"]: diff --git a/timApp/lecture/lectureanswer.py b/timApp/lecture/lectureanswer.py index e42febb028..231fd19e38 100644 --- a/timApp/lecture/lectureanswer.py +++ b/timApp/lecture/lectureanswer.py @@ -2,12 +2,12 @@ from json import JSONDecodeError from typing import Optional, TYPE_CHECKING -from sqlalchemy import func, select -from sqlalchemy.orm import lazyload, mapped_column, Mapped +from sqlalchemy import func, select, ForeignKey +from sqlalchemy.orm import lazyload, mapped_column, Mapped, relationship from timApp.lecture.lecture import Lecture from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel from timApp.user.user import User if TYPE_CHECKING: @@ -28,19 +28,21 @@ def unshuffle_lectureanswer( return unshuffled_ans -class LectureAnswer(db.Model): - __tablename__ = "lectureanswer" - +class LectureAnswer(DbModel): answer_id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) - question_id: Mapped[int] = mapped_column(db.ForeignKey("askedquestion.asked_id")) - lecture_id: Mapped[int] = mapped_column(db.ForeignKey("lecture.lecture_id")) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) + question_id: Mapped[int] = mapped_column(ForeignKey("askedquestion.asked_id")) + lecture_id: Mapped[int] = mapped_column(ForeignKey("lecture.lecture_id")) answer: Mapped[str] answered_on: Mapped[datetime_tz] points: Mapped[Optional[float]] - asked_question: Mapped["AskedQuestion"] = db.relationship(back_populates="answers", lazy="selectin") - user: Mapped["User"] = db.relationship(back_populates="lectureanswers", lazy="selectin") + asked_question: Mapped["AskedQuestion"] = relationship( + back_populates="answers", lazy="selectin" + ) + user: Mapped["User"] = relationship( + back_populates="lectureanswers", lazy="selectin" + ) @staticmethod def get_by_id(ans_id: int) -> Optional["LectureAnswer"]: diff --git a/timApp/lecture/lectureusers.py b/timApp/lecture/lectureusers.py index ff5eed6f72..a4926510aa 100644 --- a/timApp/lecture/lectureusers.py +++ b/timApp/lecture/lectureusers.py @@ -1,10 +1,11 @@ +from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel -class LectureUsers(db.Model): - __tablename__ = "lectureusers" - - lecture_id: Mapped[int] = mapped_column(db.ForeignKey("lecture.lecture_id"), primary_key=True) - user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id"), primary_key=True) +class LectureUsers(DbModel): + lecture_id: Mapped[int] = mapped_column( + ForeignKey("lecture.lecture_id"), primary_key=True + ) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) diff --git a/timApp/lecture/message.py b/timApp/lecture/message.py index 338acbaeca..bac7b5525b 100644 --- a/timApp/lecture/message.py +++ b/timApp/lecture/message.py @@ -1,26 +1,25 @@ from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.lecture.lecture import Lecture from timApp.user.user import User -class Message(db.Model): - __tablename__ = "message" +class Message(DbModel): msg_id: Mapped[int] = mapped_column(primary_key=True) - lecture_id: Mapped[int] = mapped_column(db.ForeignKey("lecture.lecture_id")) - user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + lecture_id: Mapped[int] = mapped_column(ForeignKey("lecture.lecture_id")) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) message: Mapped[str] timestamp: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) - lecture: Mapped["Lecture"] = db.relationship(back_populates="messages", lazy="select") - user: Mapped["User"] = db.relationship(back_populates="messages", lazy="select") + lecture: Mapped["Lecture"] = relationship(back_populates="messages", lazy="select") + user: Mapped["User"] = relationship(back_populates="messages", lazy="select") def to_json(self): return { diff --git a/timApp/lecture/question.py b/timApp/lecture/question.py index 760e6805d5..0a03739156 100644 --- a/timApp/lecture/question.py +++ b/timApp/lecture/question.py @@ -1,15 +1,14 @@ from typing import Optional +from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel -class Question(db.Model): - __tablename__ = "question" - +class Question(DbModel): question_id: Mapped[int] = mapped_column(primary_key=True) - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id")) par_id: Mapped[str] question_title: Mapped[str] answer: Mapped[Optional[str]] diff --git a/timApp/lecture/questionactivity.py b/timApp/lecture/questionactivity.py index 8472e47bb5..c50adb0d36 100644 --- a/timApp/lecture/questionactivity.py +++ b/timApp/lecture/questionactivity.py @@ -1,14 +1,16 @@ from enum import Enum from typing import TYPE_CHECKING -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.lecture.askedquestion import AskedQuestion from timApp.user.user import User + class QuestionActivityKind(Enum): Pointsclosed = 1 Pointsshown = 2 @@ -17,15 +19,18 @@ class QuestionActivityKind(Enum): Usershown = 5 -class QuestionActivity(db.Model): +class QuestionActivity(DbModel): __tablename__ = "question_activity" - asked_id: Mapped[int] = mapped_column(db.ForeignKey("askedquestion.asked_id"), primary_key=True) - user_id: Mapped[int] = mapped_column( - db.ForeignKey("useraccount.id"), primary_key=True + asked_id: Mapped[int] = mapped_column( + ForeignKey("askedquestion.asked_id"), primary_key=True ) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) kind: Mapped[QuestionActivityKind] = mapped_column(primary_key=True) - asked_question: Mapped["AskedQuestion"] = db.relationship(back_populates="questionactivity", lazy="select" + asked_question: Mapped["AskedQuestion"] = relationship( + back_populates="questionactivity", lazy="select" + ) + user: Mapped["User"] = relationship( + back_populates="questionactivity", lazy="select" ) - user: Mapped["User"] = db.relationship(back_populates="questionactivity", lazy="select") diff --git a/timApp/lecture/routes.py b/timApp/lecture/routes.py index 0c39045f00..7f83b08a85 100644 --- a/timApp/lecture/routes.py +++ b/timApp/lecture/routes.py @@ -50,9 +50,9 @@ qst_handle_randomization, ) from timApp.lecture.questionactivity import QuestionActivityKind, QuestionActivity -from timApp.lecture.runningquestion import Runningquestion -from timApp.lecture.showpoints import Showpoints -from timApp.lecture.useractivity import Useractivity +from timApp.lecture.runningquestion import RunningQuestion +from timApp.lecture.showpoints import ShowPoints +from timApp.lecture.useractivity import UserActivity from timApp.plugin.qst.qst import get_question_data_from_document from timApp.timdb.sqa import db, tim_main_execute from timApp.user.user import User @@ -374,7 +374,7 @@ def get_new_question( """ current_user = get_current_user_id() u = get_current_user_object() - rqs: list[Runningquestion] = lecture.running_questions + rqs: list[RunningQuestion] = lecture.running_questions with user_activity_lock(u): if rqs and rqs[0].asked_question.is_running: question: AskedQuestion = rqs[0].asked_question @@ -437,7 +437,7 @@ def get_new_question( def get_shown_points(lecture) -> AskedQuestion | None: - return lecture.asked_questions.join(Showpoints).first() + return lecture.asked_questions.join(ShowPoints).first() def check_if_lecture_is_ending(lecture: Lecture): @@ -596,7 +596,7 @@ def get_lecture_users(lecture: Lecture): students = [] activity = ( - db.session.execute(select(Useractivity).filter_by(lecture=lecture)) + db.session.execute(select(UserActivity).filter_by(lecture=lecture)) .scalars() .all() ) @@ -756,7 +756,7 @@ def delete_question_temp_data(question: AskedQuestion, lecture: Lecture): ], ) db.session.execute( - delete(Runningquestion).where(Runningquestion.lecture_id == lecture.lecture_id) + delete(RunningQuestion).where(RunningQuestion.lecture_id == lecture.lecture_id) ) stop_showing_points(lecture) @@ -846,7 +846,7 @@ def join_lecture(): def update_activity(lecture: Lecture, u: User): - ua = Useractivity(user_id=u.id, lecture_id=lecture.lecture_id, active=func.now()) + ua = UserActivity(user_id=u.id, lecture_id=lecture.lecture_id, active=func.now()) db.session.merge(ua) @@ -871,7 +871,7 @@ def extend_question(): q = get_asked_question(asked_id) if not q: raise NotExist() - rq: Runningquestion = q.running_question + rq: RunningQuestion = q.running_question if not q.is_running: raise RouteException("Question is not running") rq.end_time += timedelta(seconds=extend) @@ -959,7 +959,7 @@ def ask_question(): raise RouteException("Missing parameters") delete_question_temp_data(question, lecture) - rq = Runningquestion( + rq = RunningQuestion( lecture=lecture, asked_question=question, ask_time=question.asked_time, @@ -986,7 +986,7 @@ def show_points(m: ShowAnswerPointsModel): raise NotExist() stop_showing_points(lecture) - sp = Showpoints(asked_question=q) + sp = ShowPoints(asked_question=q) db.session.add(sp) current_question_id = m.current_question_id @@ -1000,9 +1000,9 @@ def show_points(m: ShowAnswerPointsModel): def stop_showing_points(lecture: Lecture): db.session.execute( - delete(Showpoints) + delete(ShowPoints) .where( - Showpoints.asked_id.in_( + ShowPoints.asked_id.in_( select(AskedQuestion.asked_id).filter_by(lecture_id=lecture.lecture_id) ) ) diff --git a/timApp/lecture/runningquestion.py b/timApp/lecture/runningquestion.py index a5732b0b81..1569d22e35 100644 --- a/timApp/lecture/runningquestion.py +++ b/timApp/lecture/runningquestion.py @@ -1,29 +1,29 @@ from datetime import datetime from typing import Optional, TYPE_CHECKING -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.lecture.askedquestion import AskedQuestion from timApp.lecture.lecture import Lecture -class Runningquestion(db.Model): +class RunningQuestion(DbModel): asked_id: Mapped[int] = mapped_column( - db.ForeignKey("askedquestion.asked_id"), primary_key=True + ForeignKey("askedquestion.asked_id"), primary_key=True ) lecture_id: Mapped[int] = mapped_column( - db.ForeignKey("lecture.lecture_id"), primary_key=True + ForeignKey("lecture.lecture_id"), primary_key=True ) # TODO should not be part of primary key (asked_id is enough) ask_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) end_time: Mapped[Optional[datetime_tz]] - asked_question: Mapped["AskedQuestion"] = db.relationship( + asked_question: Mapped["AskedQuestion"] = relationship( back_populates="running_question", lazy="select" ) - lecture: Mapped["Lecture"] = db.relationship( + lecture: Mapped["Lecture"] = relationship( back_populates="running_questions", lazy="select" ) diff --git a/timApp/lecture/showpoints.py b/timApp/lecture/showpoints.py index c89b703ba5..b8d8771f18 100644 --- a/timApp/lecture/showpoints.py +++ b/timApp/lecture/showpoints.py @@ -1,18 +1,19 @@ from typing import TYPE_CHECKING -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.lecture.askedquestion import AskedQuestion -class Showpoints(db.Model): - __tablename__ = "showpoints" - - asked_id: Mapped[int] = mapped_column(db.ForeignKey("askedquestion.asked_id"), primary_key=True +class ShowPoints(DbModel): + asked_id: Mapped[int] = mapped_column( + ForeignKey("askedquestion.asked_id"), primary_key=True ) - asked_question: Mapped["AskedQuestion"] = db.relationship(back_populates="showpoints", lazy="select" + asked_question: Mapped["AskedQuestion"] = relationship( + back_populates="showpoints", lazy="select" ) diff --git a/timApp/lecture/useractivity.py b/timApp/lecture/useractivity.py index f4c62072ab..4faa75014f 100644 --- a/timApp/lecture/useractivity.py +++ b/timApp/lecture/useractivity.py @@ -1,27 +1,24 @@ from typing import TYPE_CHECKING -from sqlalchemy import func -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import func, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.user.user import User from timApp.lecture.lecture import Lecture -class Useractivity(db.Model): - __tablename__ = "useractivity" - +class UserActivity(DbModel): lecture_id: Mapped[int] = mapped_column( - db.ForeignKey("lecture.lecture_id"), primary_key=True - ) - user_id: Mapped[int] = mapped_column( - db.ForeignKey("useraccount.id"), primary_key=True - ) - active: Mapped[datetime_tz] = mapped_column(default=func.now() + ForeignKey("lecture.lecture_id"), primary_key=True ) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) + active: Mapped[datetime_tz] = mapped_column(default=func.now()) - user: Mapped["User"] = db.relationship(back_populates="useractivity", lazy="select") - lecture: Mapped["Lecture"] = db.relationship(back_populates="useractivity", lazy="select") + user: Mapped["User"] = relationship(back_populates="useractivity", lazy="select") + lecture: Mapped["Lecture"] = relationship( + back_populates="useractivity", lazy="select" + ) diff --git a/timApp/messaging/messagelist/messagelist_models.py b/timApp/messaging/messagelist/messagelist_models.py index cd46c991be..7a00c52654 100644 --- a/timApp/messaging/messagelist/messagelist_models.py +++ b/timApp/messaging/messagelist/messagelist_models.py @@ -2,9 +2,9 @@ from enum import Enum from typing import Optional, Any, TYPE_CHECKING, List -from sqlalchemy import select +from sqlalchemy import select, ForeignKey from sqlalchemy.ext.hybrid import hybrid_property # type: ignore -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy.orm import mapped_column, Mapped, relationship from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound # type: ignore from timApp.messaging.messagelist.listinfo import ( @@ -16,7 +16,7 @@ MessageVerificationType, ) from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel from timApp.util.utils import get_current_time if TYPE_CHECKING: @@ -36,64 +36,64 @@ class MemberJoinMethod(Enum): """User joined the list on their own.""" -class MessageListModel(db.Model): +class MessageListModel(DbModel): """Database model for message lists""" __tablename__ = "messagelist" id: Mapped[int] = mapped_column(primary_key=True) - manage_doc_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) + manage_doc_id: Mapped[Optional[int]] = mapped_column(ForeignKey("block.id")) """The document which manages a message list.""" - name: Mapped[str] + name: Mapped[Optional[str]] """The name of a message list.""" - can_unsubscribe: Mapped[bool] + can_unsubscribe: Mapped[Optional[bool]] """If a member can unsubscribe from this list on their own.""" - email_list_domain: Mapped[str] + email_list_domain: Mapped[Optional[str]] """The domain used for an email list attached to a message list. If None/null, then message list doesn't have an attached email list. This is a tad silly at this point in time, because JYU TIM only has one domain. However, this allows quick adaptation if more domains are added or otherwise changed in the future. """ - archive: Mapped[ArchiveType] + archive: Mapped[Optional[ArchiveType]] """The archive policy of a message list.""" - notify_owner_on_change: Mapped[bool] + notify_owner_on_change: Mapped[Optional[bool]] """Should the owner of the message list be notified if there are changes on message list members.""" - description: Mapped[str] + description: Mapped[Optional[str]] """A short description what a message list is about.""" - info: Mapped[str] + info: Mapped[Optional[str]] """Additional information about the message list.""" - removed: Mapped[datetime_tz] + removed: Mapped[Optional[datetime_tz]] """When this list has been marked for removal.""" - default_send_right: Mapped[bool] + default_send_right: Mapped[Optional[bool]] """Default send right for new members who join the list on their own.""" - default_delivery_right: Mapped[bool] + default_delivery_right: Mapped[Optional[bool]] """Default delivery right for new members who join the list on their own.""" - tim_user_can_join: Mapped[bool] + tim_user_can_join: Mapped[Optional[bool]] """Flag if TIM users can join the list on their own.""" - subject_prefix: Mapped[str] + subject_prefix: Mapped[Optional[str]] """What prefix message subjects that go through the list get.""" - only_text: Mapped[bool] + only_text: Mapped[Optional[bool]] """Flag if only text format messages are allowed on a list.""" - default_reply_type: Mapped[ReplyToListChanges] + default_reply_type: Mapped[Optional[ReplyToListChanges]] """Default reply type for the list.""" - non_member_message_pass: Mapped[bool] + non_member_message_pass: Mapped[Optional[bool]] """Flag if non members messages to the list are passed straight through without moderation.""" - allow_attachments: Mapped[bool] + allow_attachments: Mapped[Optional[bool]] """Flag if attachments are allowed on the list. The list of allowed attachment file extensions are stored at listoptions.py """ @@ -102,17 +102,17 @@ class MessageListModel(db.Model): ) """How to verify messages sent to the list.""" - block: Mapped["Block"] = db.relationship( + block: Mapped["Block"] = relationship( back_populates="managed_messagelist", lazy="select" ) """Relationship to the document that is used to manage this message list.""" - members: Mapped[List["MessageListMember"]] = db.relationship( + members: Mapped[List["MessageListMember"]] = relationship( back_populates="message_list", lazy="select" ) """All the members of the list.""" - distribution: Mapped["MessageListDistribution"] = db.relationship( + distribution: Mapped["MessageListDistribution"] = relationship( back_populates="message_list", lazy="select" ) """The message channels the list uses.""" @@ -265,27 +265,27 @@ def to_info(self) -> ListInfo: ) -class MessageListMember(db.Model): +class MessageListMember(DbModel): """Database model for members of a message list.""" __tablename__ = "messagelist_member" id: Mapped[int] = mapped_column(primary_key=True) - message_list_id: Mapped[int] = mapped_column(db.ForeignKey("messagelist.id")) + message_list_id: Mapped[Optional[int]] = mapped_column(ForeignKey("messagelist.id")) """What message list a member belongs to.""" - send_right: Mapped[bool] + send_right: Mapped[Optional[bool]] """If a member can send messages to a message list.""" - delivery_right: Mapped[bool] + delivery_right: Mapped[Optional[bool]] """If a member can get messages from a message list.""" membership_ended: Mapped[Optional[datetime_tz]] """When member's membership on a list ended. This is set when member is removed from a list. A value of None means the member is still on the list.""" - join_method: Mapped[MemberJoinMethod] + join_method: Mapped[Optional[MemberJoinMethod]] """How the member came to a list.""" membership_verified: Mapped[Optional[datetime_tz]] @@ -293,24 +293,24 @@ class MessageListMember(db.Model): this date is the date teacher added the member. If the member was invited, then this is the date they verified their join. """ - member_type: Mapped[str] + member_type: Mapped[Optional[str]] """Discriminator for polymorhphic members.""" - message_list: Mapped["MessageListModel"] = db.relationship( + message_list: Mapped["MessageListModel"] = relationship( back_populates="members", lazy="select" ) - tim_member: Mapped[Optional["MessageListTimMember"]] = db.relationship( + tim_member: Mapped[Optional["MessageListTimMember"]] = relationship( back_populates="member", lazy="select", post_update=True, ) - external_member: Mapped[Optional["MessageListExternalMember"]] = db.relationship( + external_member: Mapped[Optional["MessageListExternalMember"]] = relationship( back_populates="member", lazy="select", uselist=False, post_update=True, ) - distribution: Mapped[Optional["MessageListDistribution"]] = db.relationship( + distribution: Mapped[Optional["MessageListDistribution"]] = relationship( back_populates="member", lazy="select" ) @@ -405,18 +405,18 @@ class MessageListTimMember(MessageListMember): __tablename__ = "messagelist_tim_member" id: Mapped[int] = mapped_column( - db.ForeignKey("messagelist_member.id"), primary_key=True + ForeignKey("messagelist_member.id"), primary_key=True ) - group_id: Mapped[int] = mapped_column(db.ForeignKey("usergroup.id")) + group_id: Mapped[Optional[int]] = mapped_column(ForeignKey("usergroup.id")) """A UserGroup id for a member.""" - member: Mapped["MessageListMember"] = db.relationship( + member: Mapped["MessageListMember"] = relationship( back_populates="tim_member", post_update=True, ) - user_group: Mapped["UserGroup"] = db.relationship( + user_group: Mapped["UserGroup"] = relationship( back_populates="messagelist_membership", post_update=True, ) @@ -460,18 +460,16 @@ class MessageListExternalMember(MessageListMember): __tablename__ = "messagelist_external_member" id: Mapped[int] = mapped_column( - db.ForeignKey("messagelist_member.id"), primary_key=True + ForeignKey("messagelist_member.id"), primary_key=True ) - email_address: Mapped[str] + email_address: Mapped[Optional[str]] """Email address of message list's external member.""" - display_name: Mapped[str] + display_name: Mapped[Optional[str]] """Display name for external user, which in most cases should be the external member's address' owner's name.""" - member: Mapped["MessageListMember"] = db.relationship( - back_populates="external_member" - ) + member: Mapped["MessageListMember"] = relationship(back_populates="external_member") __mapper_args__ = {"polymorphic_identity": "external_member"} @@ -501,29 +499,25 @@ def get_username(self) -> str: return "" -class MessageListDistribution(db.Model): +class MessageListDistribution(DbModel): """Message list member's chosen distribution channels.""" __tablename__ = "messagelist_distribution" id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[Optional[int]] = mapped_column( - db.ForeignKey("messagelist_member.id") - ) + user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("messagelist_member.id")) """Message list member's id, if this row is about message list member's channel distribution.""" - message_list_id: Mapped[Optional[int]] = mapped_column( - db.ForeignKey("messagelist.id") - ) + message_list_id: Mapped[Optional[int]] = mapped_column(ForeignKey("messagelist.id")) """Message list's id, if this row is about message list's channel distribution.""" - channel: Mapped[Channel] + channel: Mapped[Optional[Channel]] """Which message channels are used by a message list or a user.""" - member: Mapped[Optional["MessageListMember"]] = db.relationship( + member: Mapped[Optional["MessageListMember"]] = relationship( back_populates="distribution", lazy="select" ) - message_list: Mapped[Optional["MessageListModel"]] = db.relationship( + message_list: Mapped[Optional["MessageListModel"]] = relationship( back_populates="distribution", lazy="select" ) diff --git a/timApp/messaging/timMessage/internalmessage_models.py b/timApp/messaging/timMessage/internalmessage_models.py index 1bdd743dbb..1d6e2ead0c 100644 --- a/timApp/messaging/timMessage/internalmessage_models.py +++ b/timApp/messaging/timMessage/internalmessage_models.py @@ -2,11 +2,11 @@ from enum import Enum from typing import Any, Optional, TYPE_CHECKING, List -from sqlalchemy import func, select -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy import func, select, ForeignKey +from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.user.user import User @@ -19,18 +19,18 @@ class DisplayType(Enum): STICKY = 2 -class InternalMessage(db.Model): +class InternalMessage(DbModel): """A TIM message.""" __tablename__ = "internalmessage" - + id: Mapped[int] = mapped_column(primary_key=True) """Message identifier.""" created: Mapped[datetime_tz] = mapped_column(default=func.now()) """Date and time when the message was created.""" - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id")) """Block identifier.""" par_id: Mapped[str] @@ -45,17 +45,19 @@ class InternalMessage(db.Model): display_type: Mapped[DisplayType] """How the message is displayed.""" - expires: Mapped[Optional[datetime]] = mapped_column(db.DateTime) + expires: Mapped[Optional[datetime]] """"When the message display will disappear.""" replies_to: Mapped[Optional[int]] """Id of the message which this messages is a reply to""" - displays: Mapped[List["InternalMessageDisplay"]] = db.relationship(back_populates="message") - readreceipts: Mapped[List["InternalMessageReadReceipt"]] = db.relationship( + displays: Mapped[List["InternalMessageDisplay"]] = relationship( + back_populates="message" + ) + readreceipts: Mapped[List["InternalMessageReadReceipt"]] = relationship( back_populates="message" ) - block: Mapped["Block"] = db.relationship(back_populates="internalmessage") + block: Mapped["Block"] = relationship(back_populates="internalmessage") def to_json(self) -> dict[str, Any]: return { @@ -71,32 +73,33 @@ def to_json(self) -> dict[str, Any]: } -class InternalMessageDisplay(db.Model): +class InternalMessageDisplay(DbModel): """Where and for whom a TIM message is displayed.""" __tablename__ = "internalmessage_display" - id: Mapped[int] = mapped_column(primary_key=True) """Message display identifier.""" - message_id: Mapped[int] = mapped_column( - db.ForeignKey("internalmessage.id") - ) + message_id: Mapped[int] = mapped_column(ForeignKey("internalmessage.id")) """Message identifier.""" - usergroup_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("usergroup.id")) + usergroup_id: Mapped[Optional[int]] = mapped_column(ForeignKey("usergroup.id")) """Who sees the message; if null, displayed for everyone.""" - display_doc_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) + display_doc_id: Mapped[Optional[int]] = mapped_column(ForeignKey("block.id")) """ Identifier for the document or the folder where the message is displayed. If null, the message is displayed globally. """ - message: Mapped["InternalMessage"] = db.relationship(back_populates="displays") - usergroup: Mapped[Optional["UserGroup"]] = db.relationship(back_populates="internalmessage_display") - display_block: Mapped[Optional["Block"]] = db.relationship(back_populates="internalmessage_display") + message: Mapped["InternalMessage"] = relationship(back_populates="displays") + usergroup: Mapped[Optional["UserGroup"]] = relationship( + back_populates="internalmessage_display" + ) + display_block: Mapped[Optional["Block"]] = relationship( + back_populates="internalmessage_display" + ) def to_json(self) -> dict[str, Any]: return { @@ -107,18 +110,17 @@ def to_json(self) -> dict[str, Any]: } -class InternalMessageReadReceipt(db.Model): +class InternalMessageReadReceipt(DbModel): """Metadata about read receipts.""" __tablename__ = "internalmessage_readreceipt" - message_id: Mapped[int] = mapped_column( - db.ForeignKey("internalmessage.id"), primary_key=True + ForeignKey("internalmessage.id"), primary_key=True ) """Message identifier.""" - user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id"), primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) """Identifier for the user who marked the message as read.""" last_seen: Mapped[Optional[datetime]] @@ -127,8 +129,8 @@ class InternalMessageReadReceipt(db.Model): marked_as_read_on: Mapped[Optional[datetime]] """Timestamp for when the message was marked as read.""" - message: Mapped["InternalMessage"] = db.relationship(back_populates="readreceipts") - user: Mapped["User"] = db.relationship(back_populates="internalmessage_readreceipt") + message: Mapped["InternalMessage"] = relationship(back_populates="readreceipts") + user: Mapped["User"] = relationship(back_populates="internalmessage_readreceipt") @staticmethod def get_for_user( diff --git a/timApp/note/usernote.py b/timApp/note/usernote.py index 67ffea5700..1d17e310f2 100644 --- a/timApp/note/usernote.py +++ b/timApp/note/usernote.py @@ -1,17 +1,17 @@ from typing import Optional, TYPE_CHECKING -from sqlalchemy import func +from sqlalchemy import func, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.user.usergroup import UserGroup from timApp.item.block import Block -class UserNote(db.Model): +class UserNote(DbModel): """A comment/note that has been posted in a document paragraph.""" __tablename__ = "usernotes" @@ -19,10 +19,10 @@ class UserNote(db.Model): id: Mapped[int] = mapped_column(primary_key=True) """Comment id.""" - usergroup_id: Mapped[int] = mapped_column(db.ForeignKey("usergroup.id")) + usergroup_id: Mapped[int] = mapped_column(ForeignKey("usergroup.id")) """The UserGroup id who posted the comment.""" - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id")) """The document id in which this comment was posted.""" par_id: Mapped[str] diff --git a/timApp/notification/notification.py b/timApp/notification/notification.py index 4a1d34d8ec..5a8864197b 100644 --- a/timApp/notification/notification.py +++ b/timApp/notification/notification.py @@ -1,10 +1,12 @@ import enum from typing import TYPE_CHECKING +from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.item.block import BlockType, Block -from timApp.timdb.sqa import db, is_attribute_loaded +from timApp.timdb.sqa import is_attribute_loaded +from timApp.timdb.types import DbModel from timApp.util.logger import log_warning if TYPE_CHECKING: @@ -32,17 +34,13 @@ def is_document_modification(self) -> bool: ) -class Notification(db.Model): +class Notification(DbModel): """Notification settings for a User for a block.""" - __tablename__ = "notification" - - user_id: Mapped[int] = mapped_column( - db.ForeignKey("useraccount.id"), primary_key=True - ) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) """User id.""" - block_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + block_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) """Item id.""" notification_type: Mapped[NotificationType] = mapped_column(primary_key=True) diff --git a/timApp/notification/pending_notification.py b/timApp/notification/pending_notification.py index 8f05c3ad0b..24697ccd88 100644 --- a/timApp/notification/pending_notification.py +++ b/timApp/notification/pending_notification.py @@ -1,12 +1,12 @@ from typing import Optional, TYPE_CHECKING -from sqlalchemy import func, select +from sqlalchemy import func, select, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.document.version import Version from timApp.notification.notification import NotificationType from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.user.user import User @@ -15,12 +15,10 @@ GroupingKey = tuple[int, str] -class PendingNotification(db.Model): - __tablename__ = "pendingnotification" - +class PendingNotification(DbModel): id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id")) discriminant: Mapped[str] par_id: Mapped[Optional[str]] text: Mapped[Optional[str]] @@ -45,7 +43,7 @@ def notify_type(self) -> NotificationType: class DocumentNotification(PendingNotification): """A notification that a document has changed.""" - version_change = mapped_column(db.Text) # : str # like "1,2/1,3" + version_change: Mapped[Optional[str]] # : str # like "1,2/1,3" @property def version_before(self) -> Version: @@ -81,8 +79,8 @@ def grouping_key(self) -> GroupingKey: class AnswerNotification(PendingNotification): """A notification that an answer has been added, changed or deleted.""" - answer_number = mapped_column(db.Integer) - task_id = mapped_column(db.Text) + answer_number: Mapped[Optional[int]] + task_id: Mapped[Optional[str]] @property def grouping_key(self) -> GroupingKey: diff --git a/timApp/peerreview/peerreview.py b/timApp/peerreview/peerreview.py index 9299f238a0..f986487eb4 100644 --- a/timApp/peerreview/peerreview.py +++ b/timApp/peerreview/peerreview.py @@ -1,16 +1,16 @@ from typing import Any, Optional, TYPE_CHECKING -from sqlalchemy import UniqueConstraint +from sqlalchemy import UniqueConstraint, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.user.user import User -class PeerReview(db.Model): +class PeerReview(DbModel): """A peer review to a task.""" __tablename__ = "peer_review" @@ -18,19 +18,19 @@ class PeerReview(db.Model): id: Mapped[int] = mapped_column(primary_key=True) """Review identifier.""" - answer_id: Mapped[int] = mapped_column(db.ForeignKey("answer.id")) + answer_id: Mapped[Optional[int]] = mapped_column(ForeignKey("answer.id")) """Answer id.""" task_name: Mapped[Optional[str]] """Task name""" - block_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + block_id: Mapped[int] = mapped_column(ForeignKey("block.id")) """Doc id""" - reviewer_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + reviewer_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) """Reviewer id""" - reviewable_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + reviewable_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) """Reviewable id""" start_time: Mapped[datetime_tz] @@ -39,7 +39,7 @@ class PeerReview(db.Model): end_time: Mapped[datetime_tz] """Review end time""" - reviewed: Mapped[bool] = mapped_column(default=False) + reviewed: Mapped[Optional[bool]] = mapped_column(default=False) """Review status""" points: Mapped[Optional[float]] diff --git a/timApp/plugin/calendar/models.py b/timApp/plugin/calendar/models.py index 3a751cb412..996394d6d9 100644 --- a/timApp/plugin/calendar/models.py +++ b/timApp/plugin/calendar/models.py @@ -15,11 +15,11 @@ from dataclasses import dataclass from typing import Optional, Iterable, List, TYPE_CHECKING -from sqlalchemy import func, select +from sqlalchemy import func, select, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel from timApp.user.user import User from timApp.user.usergroup import UserGroup from tim_common.dumboclient import call_dumbo @@ -28,18 +28,16 @@ from timApp.item.block import Block -class EventGroup(db.Model): +class EventGroup(DbModel): """Information about a user group participating in an event.""" - __tablename__ = "eventgroup" - event_id: Mapped[int] = mapped_column( - db.ForeignKey("event.event_id"), primary_key=True + ForeignKey("event.event_id"), primary_key=True ) """Event the the group belongs to""" usergroup_id: Mapped[int] = mapped_column( - db.ForeignKey("usergroup.id"), primary_key=True + ForeignKey("usergroup.id"), primary_key=True ) """The usergroup that belongs to the group""" @@ -53,18 +51,16 @@ class EventGroup(db.Model): """The usergroup that belongs to the group""" -class Enrollment(db.Model): +class Enrollment(DbModel): """A single enrollment in an event""" - __tablename__ = "enrollment" - event_id: Mapped[int] = mapped_column( - db.ForeignKey("event.event_id"), primary_key=True + ForeignKey("event.event_id"), primary_key=True ) """Event the enrollment is for""" usergroup_id: Mapped[int] = mapped_column( - db.ForeignKey("usergroup.id"), primary_key=True + ForeignKey("usergroup.id"), primary_key=True ) """The usergroup that is enrolled (i.e. booked) in the event""" @@ -72,7 +68,7 @@ class Enrollment(db.Model): """The message left by the booker""" enroll_type_id: Mapped[int] = mapped_column( - db.ForeignKey("enrollmenttype.enroll_type_id") + ForeignKey("enrollmenttype.enroll_type_id") ) """Type of the enrollment""" @@ -102,26 +98,20 @@ def get_by_event_and_user( ) -class EventTagAttachment(db.Model): +class EventTagAttachment(DbModel): """Attachment information for the event tag""" - __tablename__ = "eventtagattachment" - event_id: Mapped[int] = mapped_column( - db.ForeignKey("event.event_id"), primary_key=True + ForeignKey("event.event_id"), primary_key=True ) """Event the tag is attached to""" - tag_id: Mapped[int] = mapped_column( - db.ForeignKey("eventtag.tag_id"), primary_key=True - ) + tag_id: Mapped[int] = mapped_column(ForeignKey("eventtag.tag_id"), primary_key=True) """Tag that is attached to the event""" -class EventTag(db.Model): +class EventTag(DbModel): """A string tag that can be attached to an event""" - __tablename__ = "eventtag" - tag_id: Mapped[int] = mapped_column(primary_key=True) """The id of the tag""" @@ -183,11 +173,9 @@ def is_valid(self) -> bool: return self.can_enroll or self.can_manage_event -class Event(db.Model): +class Event(DbModel): """A calendar event. Event has metadata (title, time, location) and various participating user groups.""" - __tablename__ = "event" - event_id: Mapped[int] = mapped_column(primary_key=True) """Identification number of the event""" @@ -212,13 +200,13 @@ class Event(db.Model): signup_before: Mapped[Optional[datetime_tz]] """Time until signup is closed""" - creator_user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + creator_user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) """User who created the event originally""" - origin_doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + origin_doc_id: Mapped[Optional[int]] = mapped_column(ForeignKey("block.id")) """Document that was used to create the event""" - origin_doc: Mapped["Block"] = relationship() + origin_doc: Mapped[Optional["Block"]] = relationship() """Document that was used to create the event""" enrolled_users: Mapped[List["UserGroup"]] = relationship( @@ -415,11 +403,9 @@ def to_json( } -class EnrollmentType(db.Model): +class EnrollmentType(DbModel): """Table for enrollment type, combines enrollment type ID to specific enrollment type""" - __tablename__ = "enrollmenttype" - enroll_type_id: Mapped[int] = mapped_column(primary_key=True) """Enrollment type""" @@ -427,14 +413,10 @@ class EnrollmentType(db.Model): """Name of the enrollment type""" -class ExportedCalendar(db.Model): +class ExportedCalendar(DbModel): """Information about exported calendars""" - __tablename__ = "exportedcalendar" - - user_id: Mapped[int] = mapped_column( - db.ForeignKey("useraccount.id"), primary_key=True - ) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) """User who created the exported calendar""" calendar_hash: Mapped[str] diff --git a/timApp/plugin/pluginControl.py b/timApp/plugin/pluginControl.py index 47ad9b4562..69e4252074 100644 --- a/timApp/plugin/pluginControl.py +++ b/timApp/plugin/pluginControl.py @@ -9,7 +9,7 @@ import attr import yaml import yaml.parser -from sqlalchemy import func +from sqlalchemy import func, select from timApp.answer.answer import Answer from timApp.answer.answers import valid_answers_query, valid_taskid_filter @@ -43,6 +43,8 @@ from timApp.plugin.pluginexception import PluginException from timApp.plugin.taskid import TaskId from timApp.printing.printsettings import PrintFormat +from timApp.timdb.sqa import db +from timApp.user.user import User from timApp.util.get_fields import ( get_fields_and_users, RequestedGroups, @@ -376,8 +378,7 @@ def check_task_access(errs: ErrorMap, p_range: Range, plugin_name: str, tid: Tas KeyType = tuple[int, Range] -def get_answers(user, task_ids, answer_map): - # FIXME: SQLAlchemy dynamic +def get_answers(user: User, task_ids, answer_map): col = func.max(Answer.id).label("col") cnt = func.count(Answer.id).label("cnt") if user is None: @@ -396,11 +397,11 @@ def get_answers(user, task_ids, answer_map): .group_by(Answer.task_id) .subquery() ) - answers: list[tuple[Answer, int]] = ( - Answer.query.join(sub, Answer.id == sub.c.col) - .with_entities(Answer, sub.c.cnt) - .all() - ) + answers: list[tuple[Answer, int]] = db.session.execute( + select(Answer) + .join(sub, Answer.id == sub.c.col) + .with_only_columns(Answer, sub.c.cnt) + ).all() for answer, cnt in answers: answer_map[answer.task_id] = answer, cnt return cnt, answers diff --git a/timApp/plugin/plugintype.py b/timApp/plugin/plugintype.py index f5966715a5..12e6d89765 100644 --- a/timApp/plugin/plugintype.py +++ b/timApp/plugin/plugintype.py @@ -4,10 +4,11 @@ import filelock from sqlalchemy import select from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import mapped_column, Mapped +from sqlalchemy.orm import mapped_column, Mapped, Session import timApp from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel CONTENT_FIELD_NAME_MAP = { "csPlugin": "usercode", @@ -34,9 +35,7 @@ def to_json(self) -> dict[str, Any]: # TODO: Right now values are added dynamically to the table when saving answers. Instead add them on TIM start. -class PluginType(db.Model, PluginTypeBase): - __tablename__ = "plugintype" - +class PluginType(DbModel, PluginTypeBase): id: Mapped[int] = mapped_column(primary_key=True) type: Mapped[str] = mapped_column(unique=True) @@ -55,11 +54,9 @@ def resolve(p_type: str) -> "PluginType": # Use a lock to prevent concurrent access with filelock.FileLock("/tmp/plugin_type_create.lock"): try: - tmp_session = db._make_session_factory({}) - session = tmp_session() - session.add(PluginType(type=p_type)) - session.commit() - session.close() + with Session(db.engine) as session: + session.add(PluginType(type=p_type)) + session.commit() except IntegrityError as e: # TODO: Try to debug why this still happens even after locking if ( diff --git a/timApp/plugin/timtable/row_owner_info.py b/timApp/plugin/timtable/row_owner_info.py index ec0e159665..6233d79777 100644 --- a/timApp/plugin/timtable/row_owner_info.py +++ b/timApp/plugin/timtable/row_owner_info.py @@ -1,22 +1,21 @@ from typing import Optional +from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel -class RowOwnerInfo(db.Model): +class RowOwnerInfo(DbModel): """ Information about the owner of a TimTable row. Includes document and paragraph id for determining the TimTable instance. """ - __tablename__ = "rowownerinfo" - doc_id: Mapped[int] = mapped_column(primary_key=True) par_id: Mapped[str] = mapped_column(primary_key=True) unique_row_id: Mapped[int] = mapped_column(primary_key=True) - usergroup_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("usergroup.id")) + usergroup_id: Mapped[Optional[int]] = mapped_column(ForeignKey("usergroup.id")) # usergroup = relationship('UserGroup', back_populates='rowOwnerInfo') # block = relationship('Block', back_populates='tags') diff --git a/timApp/printing/printeddoc.py b/timApp/printing/printeddoc.py index 9d257bf7fa..7148cd1f68 100644 --- a/timApp/printing/printeddoc.py +++ b/timApp/printing/printeddoc.py @@ -1,29 +1,29 @@ from typing import Optional -from sqlalchemy import func +from sqlalchemy import func, ForeignKey from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel -class PrintedDoc(db.Model): +class PrintedDoc(DbModel): """A printed document. A PrintedDoc is created each time a document is printed (CSS printing does not count because it happens entirely in browser).""" __tablename__ = "printed_doc" id: Mapped[int] = mapped_column(primary_key=True) - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id")) """Id of the printed document.""" - template_doc_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) + template_doc_id: Mapped[Optional[int]] = mapped_column(ForeignKey("block.id")) """Id of the template document.""" file_type: Mapped[str] """The filetype of the print.""" - path_to_file: Mapped[str] + path_to_file: Mapped[Optional[str]] """Path to the printed document in the filesystem.""" version: Mapped[str] diff --git a/timApp/readmark/readparagraph.py b/timApp/readmark/readparagraph.py index cf0531facb..bdd3966480 100644 --- a/timApp/readmark/readparagraph.py +++ b/timApp/readmark/readparagraph.py @@ -1,28 +1,25 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional -from sqlalchemy import func +from sqlalchemy import func, ForeignKey, Index from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.readmark.readparagraphtype import ReadParagraphType -from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.user.usergroup import UserGroup -class ReadParagraph(db.Model): +class ReadParagraph(DbModel): """Denotes that a User(Group) has read a specific paragraph in some way.""" - __tablename__ = "readparagraph" - id: Mapped[int] = mapped_column(primary_key=True) """Readmark id.""" - usergroup_id: Mapped[int] = mapped_column(db.ForeignKey("usergroup.id")) + usergroup_id: Mapped[int] = mapped_column(ForeignKey("usergroup.id")) """UserGroup id.""" - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id")) + doc_id: Mapped[Optional[int]] = mapped_column(ForeignKey("block.id")) """Document id.""" par_id: Mapped[str] @@ -38,8 +35,8 @@ class ReadParagraph(db.Model): """The time the readmark was registered.""" __table_args__ = ( - db.Index("readparagraph_doc_id_par_id_idx", "doc_id", "par_id"), - db.Index("readparagraph_doc_id_usergroup_id_idx", "doc_id", "usergroup_id"), + Index("readparagraph_doc_id_par_id_idx", "doc_id", "par_id"), + Index("readparagraph_doc_id_usergroup_id_idx", "doc_id", "usergroup_id"), ) usergroup: Mapped["UserGroup"] = relationship(back_populates="readparagraphs") diff --git a/timApp/sisu/scimusergroup.py b/timApp/sisu/scimusergroup.py index 85094ca6c5..20ad7ee77f 100644 --- a/timApp/sisu/scimusergroup.py +++ b/timApp/sisu/scimusergroup.py @@ -1,8 +1,9 @@ import re +from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel uuid_re = "[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}" external_id_re = re.compile( @@ -10,12 +11,8 @@ ) -class ScimUserGroup(db.Model): - __tablename__ = "scimusergroup" - - group_id: Mapped[int] = mapped_column( - db.ForeignKey("usergroup.id"), primary_key=True - ) +class ScimUserGroup(DbModel): + group_id: Mapped[int] = mapped_column(ForeignKey("usergroup.id"), primary_key=True) external_id: Mapped[str] = mapped_column(unique=True) @property diff --git a/timApp/slide/slidestatus.py b/timApp/slide/slidestatus.py index 789b48a61c..7ab43a08c9 100644 --- a/timApp/slide/slidestatus.py +++ b/timApp/slide/slidestatus.py @@ -1,14 +1,11 @@ +from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel -class SlideStatus(db.Model): +class SlideStatus(DbModel): __tablename__ = "slide_status" - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) status: Mapped[str] - - def __init__(self, doc_id, status): - self.doc_id = doc_id - self.status = status diff --git a/timApp/tests/server/test_lecture.py b/timApp/tests/server/test_lecture.py index 22697268d0..e84e19b422 100644 --- a/timApp/tests/server/test_lecture.py +++ b/timApp/tests/server/test_lecture.py @@ -8,7 +8,7 @@ from timApp.lecture.askedquestion import AskedQuestion, get_asked_question from timApp.lecture.lecture import Lecture from timApp.lecture.lectureanswer import LectureAnswer -from timApp.lecture.showpoints import Showpoints +from timApp.lecture.showpoints import ShowPoints from timApp.tests.db.timdbtest import TEST_USER_1_ID from timApp.tests.server.timroutetest import TimRouteTest from timApp.timdb.sqa import db @@ -266,14 +266,14 @@ def test_lecture(self): new_end_time = dateutil.parser.parse(resp["question_end_time"]) self.assertTrue(original_end_time > new_end_time) - sp = db.session.get(Showpoints, aid) + sp = db.session.get(ShowPoints, aid) self.assertIsNone(sp) self.login_test1() self.post("/showAnswerPoints", query_string=dict(asked_id=aid)) db.session.remove() - sp = db.session.get(Showpoints, aid) + sp = db.session.get(ShowPoints, aid) self.assertIsNotNone(sp) resp = self.get_updates(doc.id, msg_id, True, aid) diff --git a/timApp/tim_app.py b/timApp/tim_app.py index 0f25526ac5..e9602f70fa 100644 --- a/timApp/tim_app.py +++ b/timApp/tim_app.py @@ -55,9 +55,9 @@ from timApp.lecture.message import Message from timApp.lecture.question import Question from timApp.lecture.questionactivity import QuestionActivity -from timApp.lecture.runningquestion import Runningquestion -from timApp.lecture.showpoints import Showpoints -from timApp.lecture.useractivity import Useractivity +from timApp.lecture.runningquestion import RunningQuestion +from timApp.lecture.showpoints import ShowPoints +from timApp.lecture.useractivity import UserActivity from timApp.messaging.messagelist.messagelist_models import ( MessageListModel, MessageListMember, @@ -198,9 +198,9 @@ RegisteredTranslationService, ReversingTranslationService, RowOwnerInfo, - Runningquestion, + RunningQuestion, ScimUserGroup, - Showpoints, + ShowPoints, SlideStatus, Tag, TaskBlock, @@ -208,7 +208,7 @@ TranslationService, TranslationServiceKey, User, - Useractivity, + UserActivity, UserAnswer, UserContact, UserGroup, @@ -298,15 +298,17 @@ def print_schema(bind: str | None = None): :param bind: The bind to use. """ - models = inspect.getmembers( - sys.modules[__name__], lambda x: inspect.isclass(x) and hasattr(x, "__table__") - ) - eng = db.engines[bind] + with app.app_context(): + models = inspect.getmembers( + sys.modules[__name__], + lambda x: inspect.isclass(x) and hasattr(x, "__table__"), + ) + eng = db.engines[bind] - for _, model_class in models: - print(CreateTable(model_class.__table__).compile(eng), end=";") - print() - sys.stdout.flush() + for _, model_class in models: + print(CreateTable(model_class.__table__).compile(eng), end=";") + print() + sys.stdout.flush() # print_schema() diff --git a/timApp/timdb/sqa.py b/timApp/timdb/sqa.py index d3e4309f01..56c215d487 100644 --- a/timApp/timdb/sqa.py +++ b/timApp/timdb/sqa.py @@ -1,4 +1,4 @@ -"""Defines the SQLAlchemy object "db" that is used by all model classes by inheriting from db.Model. +"""Defines the SQLAlchemy object "db" that is used by all model classes by inheriting from DbModel. __tablename__ is not mandatory but recommended in order to maintain the naming convention for tables. The default table name is class name in lowercase. @@ -14,7 +14,7 @@ from sqlalchemy.orm import mapped_column from sqlalchemy.orm.base import instance_state, Mapped -from timApp.timdb.types import add_tim_types, datetime_tz +from timApp.timdb.types import datetime_tz, DbModel session_options = { "future": True, @@ -27,10 +27,17 @@ # because sometimes objects would expire after calling a route. session_options["expire_on_commit"] = False -db = SQLAlchemy(session_options=session_options, engine_options=engine_options) -add_tim_types(db) +db = SQLAlchemy( + session_options=session_options, engine_options=engine_options, model_class=DbModel +) +# Overwrite metadata to use the DbModel's metadata +# Flask-SQLAlchemy 3.x doesn't appear to have a correct handler of model_class, so it ends up overwriting our DbModel +# Instead, we pass our model manually +db.Model = DbModel +db.metadatas[None] = DbModel.metadata -# TODO: Replace db.Model with custom DeclarativeBase class that also specifies __tablename__ and custom types. + +# TODO: Replace DbModel with custom DeclarativeBase class that also specifies __tablename__ and custom types. # See https://docs.sqlalchemy.org/en/20/orm/declarative_mixins.html # TODO: Switch models to use dataclasses instead # See https://docs.sqlalchemy.org/en/20/orm/dataclasses.html#declarative-dataclass-mapping diff --git a/timApp/timdb/types.py b/timApp/timdb/types.py index c0d69f3883..8fc9efd357 100644 --- a/timApp/timdb/types.py +++ b/timApp/timdb/types.py @@ -1,16 +1,25 @@ from datetime import datetime -from flask_sqlalchemy import SQLAlchemy +from flask_sqlalchemy.model import Model +from sqlalchemy import Text, DateTime +from sqlalchemy.orm import DeclarativeBase, declared_attr, has_inherited_table from typing_extensions import Annotated datetime_tz = Annotated[datetime, "datetime_tz"] -def add_tim_types(db: SQLAlchemy) -> None: - # In TIM, we always use TEXT by default for strings - db.Model.registry.update_type_annotation_map( - { - str: db.Text, - datetime_tz: db.DateTime(timezone=True), - } - ) +class DbModel(DeclarativeBase, Model): + """ + Base class for all TIM database models. + """ + + type_annotation_map = { + str: Text, + datetime_tz: DateTime(timezone=True), + } + + @declared_attr.directive + def __tablename__(cls) -> str | None: + if has_inherited_table(cls): + return None + return cls.__name__.lower() diff --git a/timApp/user/consentchange.py b/timApp/user/consentchange.py index fd4917d72e..a88d9f8da3 100644 --- a/timApp/user/consentchange.py +++ b/timApp/user/consentchange.py @@ -1,16 +1,13 @@ -from sqlalchemy import func +from sqlalchemy import func, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel from timApp.user.user import Consent, User -class ConsentChange(db.Model): - __tablename__ = "consentchange" - +class ConsentChange(DbModel): id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) time: Mapped[datetime_tz] = mapped_column(default=func.now()) consent: Mapped[Consent] diff --git a/timApp/user/hakaorganization.py b/timApp/user/hakaorganization.py index 1ccd2771f9..432bb0faa7 100644 --- a/timApp/user/hakaorganization.py +++ b/timApp/user/hakaorganization.py @@ -6,12 +6,15 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.user.personaluniquecode import PersonalUniqueCode -class HakaOrganization(db.Model): +class HakaOrganization(DbModel): + __tablename__ = "haka_organization" + id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(unique=True) diff --git a/timApp/user/newuser.py b/timApp/user/newuser.py index 3d8f3ac4d4..344cf77084 100644 --- a/timApp/user/newuser.py +++ b/timApp/user/newuser.py @@ -1,16 +1,13 @@ from sqlalchemy import func from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel from timApp.user.userutils import check_password_hash -class NewUser(db.Model): +class NewUser(DbModel): """A user that is going to register to TIM via email and has not yet completed the registration process.""" - __tablename__ = "newuser" - email: Mapped[str] = mapped_column(primary_key=True) """Email address.""" diff --git a/timApp/user/personaluniquecode.py b/timApp/user/personaluniquecode.py index d7910127c2..9b14a5e06d 100644 --- a/timApp/user/personaluniquecode.py +++ b/timApp/user/personaluniquecode.py @@ -2,26 +2,27 @@ from dataclasses import dataclass from typing import Optional, TYPE_CHECKING -from sqlalchemy import select, UniqueConstraint +from sqlalchemy import select, UniqueConstraint, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel from timApp.user.hakaorganization import HakaOrganization if TYPE_CHECKING: from timApp.user.user import User -class PersonalUniqueCode(db.Model): +class PersonalUniqueCode(DbModel): """The database model for the 'schacPersonalUniqueCode' Haka attribute.""" - user_id: Mapped[int] = mapped_column( - db.ForeignKey("useraccount.id"), primary_key=True - ) + __tablename__ = "personal_unique_code" + + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) """User id.""" org_id: Mapped[int] = mapped_column( - db.ForeignKey("haka_organization.id"), primary_key=True + ForeignKey("haka_organization.id"), primary_key=True ) """Organization id.""" diff --git a/timApp/user/user.py b/timApp/user/user.py index a8f120ee60..7a429e5555 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -41,6 +41,7 @@ from timApp.sisu.scimusergroup import ScimUserGroup from timApp.timdb.exceptions import TimDbException from timApp.timdb.sqa import db, TimeStampMixin, is_attribute_loaded +from timApp.timdb.types import DbModel from timApp.user.hakaorganization import HakaOrganization, get_home_organization_id from timApp.user.personaluniquecode import SchacPersonalUniqueCode, PersonalUniqueCode from timApp.user.preferences import Preferences @@ -92,7 +93,7 @@ from timApp.lecture.lectureanswer import LectureAnswer from timApp.lecture.message import Message from timApp.lecture.questionactivity import QuestionActivity - from timApp.lecture.useractivity import Useractivity + from timApp.lecture.useractivity import UserActivity from timApp.velp.annotation_model import Annotation from timApp.velp.velp_models import Velp @@ -257,7 +258,7 @@ def user_query_with_joined_groups() -> Select: return select(User).options(selectinload(User.groups)) -class User(db.Model, TimeStampMixin, SCIMEntity): +class User(DbModel, TimeStampMixin, SCIMEntity): """A user account. Used to identify users. .. note:: Some user IDs are reserved for internal use: @@ -421,7 +422,7 @@ def _set_email(self, value: str) -> None: ) """User's activity on lecture questions.""" - useractivity: Mapped[List["Useractivity"]] = relationship(back_populates="user") + useractivity: Mapped[List["UserActivity"]] = relationship(back_populates="user") """User's activity during lectures.""" answers: DynamicMapped["Answer"] = relationship( diff --git a/timApp/user/usercontact.py b/timApp/user/usercontact.py index c66949bccb..078a3215c6 100644 --- a/timApp/user/usercontact.py +++ b/timApp/user/usercontact.py @@ -1,11 +1,12 @@ from enum import Enum from typing import Optional, TYPE_CHECKING, List -from sqlalchemy import UniqueConstraint +from sqlalchemy import UniqueConstraint, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.messaging.messagelist.listinfo import Channel from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.user.user import User @@ -36,11 +37,9 @@ class PrimaryContact(Enum): true = True -class UserContact(db.Model): +class UserContact(DbModel): """TIM users' additional contact information.""" - __tablename__ = "usercontact" - __table_args__ = ( # A user should not have the same contact for the channel # Different users are fine though @@ -64,7 +63,7 @@ class UserContact(db.Model): id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) """Which user owns this contact information.""" contact: Mapped[str] diff --git a/timApp/user/usergroup.py b/timApp/user/usergroup.py index 371cec443c..cfadfb003e 100644 --- a/timApp/user/usergroup.py +++ b/timApp/user/usergroup.py @@ -18,6 +18,7 @@ from timApp.sisu.parse_display_name import parse_sisu_group_display_name from timApp.sisu.scimusergroup import ScimUserGroup from timApp.timdb.sqa import db, TimeStampMixin, include_if_exists, is_attribute_loaded +from timApp.timdb.types import DbModel from timApp.user.scimentity import SCIMEntity from timApp.user.special_group_names import ( ANONYMOUS_GROUPNAME, @@ -57,7 +58,7 @@ def tim_group_to_scim(tim_group: str) -> str: ORG_GROUP_SUFFIX = " users" -class UserGroup(db.Model, TimeStampMixin, SCIMEntity): +class UserGroup(DbModel, TimeStampMixin, SCIMEntity): """A usergroup. Each User should belong to a personal UserGroup that has the same name as the User name. No one else should belong to a personal UserGroup. @@ -70,8 +71,6 @@ class UserGroup(db.Model, TimeStampMixin, SCIMEntity): the two groups are empty from the database's point of view. """ - __tablename__ = "usergroup" - id: Mapped[int] = mapped_column(primary_key=True) """Usergroup identifier.""" diff --git a/timApp/user/usergroupdoc.py b/timApp/user/usergroupdoc.py index 03a993ebce..6e3fc62d6e 100644 --- a/timApp/user/usergroupdoc.py +++ b/timApp/user/usergroupdoc.py @@ -1,16 +1,13 @@ +from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.sqa import db +from timApp.timdb.types import DbModel -class UserGroupDoc(db.Model): +class UserGroupDoc(DbModel): """Each UserGroup can have at most one administrative document. The rights of that document determine who can see and edit the members of the UserGroup. """ - __tablename__ = "usergroupdoc" - - group_id: Mapped[int] = mapped_column( - db.ForeignKey("usergroup.id"), primary_key=True - ) - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + group_id: Mapped[int] = mapped_column(ForeignKey("usergroup.id"), primary_key=True) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) diff --git a/timApp/user/usergroupmember.py b/timApp/user/usergroupmember.py index 301af45bb6..c262acd3da 100644 --- a/timApp/user/usergroupmember.py +++ b/timApp/user/usergroupmember.py @@ -13,11 +13,10 @@ from datetime import timedelta from typing import Optional, TYPE_CHECKING -from sqlalchemy import func +from sqlalchemy import func, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel from timApp.util.utils import get_current_time if TYPE_CHECKING: @@ -25,21 +24,17 @@ from timApp.user.usergroup import UserGroup -class UserGroupMember(db.Model): +class UserGroupMember(DbModel): """ Associates a user with a user group. """ - __tablename__ = "usergroupmember" - usergroup_id: Mapped[int] = mapped_column( - db.ForeignKey("usergroup.id"), primary_key=True + ForeignKey("usergroup.id"), primary_key=True ) """ID of the usergroup the member belongs to.""" - user_id: Mapped[int] = mapped_column( - db.ForeignKey("useraccount.id"), primary_key=True - ) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) """ID of the user that belongs to the usergroup.""" membership_end: Mapped[Optional[datetime_tz]] @@ -49,14 +44,16 @@ class UserGroupMember(db.Model): If the end timestamp is present, the user is considered deleted from the group. """ - membership_added: Mapped[datetime_tz] = mapped_column(default=get_current_time) + membership_added: Mapped[Optional[datetime_tz]] = mapped_column( + default=get_current_time + ) """Timestamp for when the user was last time added as the active member. .. note:: The timestamp is used **for logging purposes only**. In other words, it is not used to determine soft deletion or other membership state. """ - added_by: Mapped[Optional[int]] = mapped_column(db.ForeignKey("useraccount.id")) + added_by: Mapped[Optional[int]] = mapped_column(ForeignKey("useraccount.id")) """User ID of the user who added the membership.""" user: Mapped["User"] = relationship(foreign_keys=[user_id]) diff --git a/timApp/user/verification/verification.py b/timApp/user/verification/verification.py index 9ea1f4ca35..b630c5b5a0 100644 --- a/timApp/user/verification/verification.py +++ b/timApp/user/verification/verification.py @@ -4,12 +4,12 @@ from typing import Optional from flask import render_template_string, url_for -from sqlalchemy import select +from sqlalchemy import select, ForeignKey from sqlalchemy.orm import load_only, mapped_column, Mapped, relationship from timApp.document.docentry import DocEntry from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel from timApp.user.user import User from timApp.user.usercontact import UserContact, PrimaryContact from timApp.util.utils import get_current_time @@ -46,19 +46,17 @@ def parse(t: str) -> Optional["VerificationType"]: } -class Verification(db.Model): +class Verification(DbModel): """For various pending verifications, such as message list joining and contact information ownership verification.""" - __tablename__ = "verification" - token: Mapped[str] = mapped_column(primary_key=True) """Verification token used for action verification""" type: Mapped[VerificationType] = mapped_column(primary_key=True) """The type of verification, see VerificationType class for details.""" - user_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) """User that can react to verification request.""" requested_at: Mapped[Optional[datetime_tz]] @@ -87,11 +85,9 @@ def to_json(self) -> dict: class ContactAddVerification(Verification): - contact_id = mapped_column(db.Integer, db.ForeignKey("usercontact.id")) + contact_id: Mapped[Optional[int]] = mapped_column(ForeignKey("usercontact.id")) - contact = db.relationship( - "UserContact", lazy="select", uselist=False - ) # : UserContact | None + contact: Mapped[Optional["UserContact"]] = relationship() """Contact to verify.""" @property diff --git a/timApp/velp/annotation_model.py b/timApp/velp/annotation_model.py index 0fba07b462..a2b9fd1447 100644 --- a/timApp/velp/annotation_model.py +++ b/timApp/velp/annotation_model.py @@ -3,10 +3,10 @@ from datetime import datetime from typing import Optional, TYPE_CHECKING, List +from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.user.user import User @@ -45,22 +45,20 @@ class AnnotationPosition: end: AnnotationCoordinate -class Annotation(db.Model): +class Annotation(DbModel): """An annotation that can be associated with an Answer or with a DocParagraph in a Document. The annotation can start and end in specific positions, in which case the annotation is supposed to be displayed as highlighted text in the corresponding location. """ - __tablename__ = "annotation" - id: Mapped[int] = mapped_column(primary_key=True) """Annotation identifier.""" - velp_version_id: Mapped[int] = mapped_column(db.ForeignKey("velpversion.id")) + velp_version_id: Mapped[int] = mapped_column(ForeignKey("velpversion.id")) """Id of the velp that has been used for this annotation.""" - annotator_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + annotator_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) """Id of the User who created the annotation.""" points: Mapped[Optional[float]] @@ -87,10 +85,10 @@ class Annotation(db.Model): """ - document_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("block.id")) + document_id: Mapped[Optional[int]] = mapped_column(ForeignKey("block.id")) """Id of the document in case this is a paragraph annotation.""" - answer_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("answer.id")) + answer_id: Mapped[Optional[int]] = mapped_column(ForeignKey("answer.id")) """Id of the Answer in case this is an answer annotation.""" paragraph_id_start: Mapped[Optional[str]] diff --git a/timApp/velp/velp_models.py b/timApp/velp/velp_models.py index 0c6455c650..f032907b36 100644 --- a/timApp/velp/velp_models.py +++ b/timApp/velp/velp_models.py @@ -2,24 +2,22 @@ from datetime import datetime from typing import Optional, TYPE_CHECKING, Dict, List +from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship, attribute_keyed_dict from sqlalchemy.orm.collections import attribute_mapped_collection # type: ignore from timApp.item.block import Block -from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz +from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: from timApp.user.user import User -class VelpContent(db.Model): +class VelpContent(DbModel): """The actual content of a Velp.""" - __tablename__ = "velpcontent" - version_id: Mapped[int] = mapped_column( - db.ForeignKey("velpversion.id"), primary_key=True + ForeignKey("velpversion.id"), primary_key=True ) language_id: Mapped[str] = mapped_column(primary_key=True) content: Mapped[Optional[str]] @@ -28,21 +26,19 @@ class VelpContent(db.Model): velp_version: Mapped["VelpVersion"] = relationship() -class AnnotationComment(db.Model): +class AnnotationComment(DbModel): """A comment in an Annotation.""" - __tablename__ = "annotationcomment" - id: Mapped[int] = mapped_column(primary_key=True) """Comment identifier.""" - annotation_id: Mapped[int] = mapped_column(db.ForeignKey("annotation.id")) + annotation_id: Mapped[int] = mapped_column(ForeignKey("annotation.id")) """Annotation id.""" comment_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) """Comment timestamp.""" - commenter_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + commenter_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) """Commenter user id.""" content: Mapped[Optional[str]] @@ -60,36 +56,28 @@ def to_json(self) -> dict: } -class LabelInVelp(db.Model): +class LabelInVelp(DbModel): """Associates VelpLabels with Velps.""" - __tablename__ = "labelinvelp" - - label_id: Mapped[int] = mapped_column( - db.ForeignKey("velplabel.id"), primary_key=True - ) - velp_id: Mapped[int] = mapped_column(db.ForeignKey("velp.id"), primary_key=True) - + label_id: Mapped[int] = mapped_column(ForeignKey("velplabel.id"), primary_key=True) + velp_id: Mapped[int] = mapped_column(ForeignKey("velp.id"), primary_key=True) -class VelpInGroup(db.Model): - __tablename__ = "velpingroup" +class VelpInGroup(DbModel): velp_group_id: Mapped[int] = mapped_column( - db.ForeignKey("velpgroup.id"), primary_key=True + ForeignKey("velpgroup.id"), primary_key=True ) - velp_id: Mapped[int] = mapped_column(db.ForeignKey("velp.id"), primary_key=True) + velp_id: Mapped[int] = mapped_column(ForeignKey("velp.id"), primary_key=True) -class Velp(db.Model): +class Velp(DbModel): """A Velp is a kind of category for Annotations and is visually represented by a Post-it note.""" - __tablename__ = "velp" - id: Mapped[int] = mapped_column(primary_key=True) - creator_id: Mapped[int] = mapped_column(db.ForeignKey("useraccount.id")) + creator_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) creation_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) default_points: Mapped[Optional[float]] - valid_from: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) + valid_from: Mapped[Optional[datetime_tz]] = mapped_column(default=datetime.utcnow) valid_until: Mapped[Optional[datetime_tz]] color: Mapped[Optional[str]] visible_to: Mapped[int] @@ -129,25 +117,23 @@ def to_json(self) -> dict: } -class VelpGroup(db.Model): +class VelpGroup(DbModel): """Represents a group of Velps.""" - __tablename__ = "velpgroup" - - id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) name: Mapped[Optional[str]] creation_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) - valid_from: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) + valid_from: Mapped[Optional[datetime_tz]] = mapped_column(default=datetime.utcnow) valid_until: Mapped[Optional[datetime_tz]] - default_group: Mapped[bool] = mapped_column(default=False) + default_group: Mapped[Optional[bool]] = mapped_column(default=False) - velps: Mapped[Dict[int, "Velp"]] = db.relationship( + velps: Mapped[Dict[int, "Velp"]] = relationship( back_populates="groups", secondary=VelpInGroup.__table__, collection_class=attribute_keyed_dict("id"), cascade="all", ) - block: Mapped["Block"] = db.relationship(lazy="joined") + block: Mapped["Block"] = relationship(lazy="joined") def to_json(self) -> dict: return { @@ -157,68 +143,54 @@ def to_json(self) -> dict: } -class VelpGroupDefaults(db.Model): - __tablename__ = "velpgroupdefaults" - - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) +class VelpGroupDefaults(DbModel): + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) target_id: Mapped[str] = mapped_column(primary_key=True) velp_group_id: Mapped[int] = mapped_column( - db.ForeignKey("velpgroup.id"), primary_key=True + ForeignKey("velpgroup.id"), primary_key=True ) target_type: Mapped[int] # 0 = document, 1 = paragraph, 2 = area - selected: Mapped[bool] = mapped_column(default=False) + selected: Mapped[Optional[bool]] = mapped_column(default=False) -class VelpGroupLabel(db.Model): +class VelpGroupLabel(DbModel): """Currently not used (0 rows in production DB as of 5th July 2018).""" - __tablename__ = "velpgrouplabel" - id: Mapped[int] = mapped_column(primary_key=True) content: Mapped[str] -class VelpGroupSelection(db.Model): - __tablename__ = "velpgroupselection" - - user_id: Mapped[int] = mapped_column( - db.ForeignKey("useraccount.id"), primary_key=True - ) - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) +class VelpGroupSelection(DbModel): + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) target_id: Mapped[str] = mapped_column(primary_key=True) target_type: Mapped[int] # 0 = document, 1 = paragraph, 2 = area - selected: Mapped[bool] = mapped_column(default=False) + selected: Mapped[Optional[bool]] = mapped_column(default=False) velp_group_id: Mapped[int] = mapped_column( - db.ForeignKey("velpgroup.id"), primary_key=True + ForeignKey("velpgroup.id"), primary_key=True ) -class VelpGroupsInDocument(db.Model): +class VelpGroupsInDocument(DbModel): """ TODO: This table contains lots of rows in production DB (about 19000 as of 5th July 2018). TODO: Possibly needs some optimizations. """ - __tablename__ = "velpgroupsindocument" - - user_id: Mapped[int] = mapped_column( - db.ForeignKey("useraccount.id"), primary_key=True - ) - doc_id: Mapped[int] = mapped_column(db.ForeignKey("block.id"), primary_key=True) + user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) + doc_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) velp_group_id: Mapped[int] = mapped_column( - db.ForeignKey("velpgroup.id"), primary_key=True + ForeignKey("velpgroup.id"), primary_key=True ) -class VelpLabel(db.Model): +class VelpLabel(DbModel): """A label that can be assigned to a Velp.""" - __tablename__ = "velplabel" - id: Mapped[int] = mapped_column(primary_key=True) # TODO make not optional - creator_id: Mapped[Optional[int]] = mapped_column(db.ForeignKey("useraccount.id")) + creator_id: Mapped[Optional[int]] = mapped_column(ForeignKey("useraccount.id")) creator: Mapped[Optional["User"]] = relationship() velps: Mapped[Dict[int, "Velp"]] = relationship( @@ -228,11 +200,9 @@ class VelpLabel(db.Model): ) -class VelpLabelContent(db.Model): - __tablename__ = "velplabelcontent" - +class VelpLabelContent(DbModel): velplabel_id: Mapped[int] = mapped_column( - db.ForeignKey("velplabel.id"), primary_key=True + ForeignKey("velplabel.id"), primary_key=True ) language_id: Mapped[str] = mapped_column(primary_key=True) content: Mapped[Optional[str]] @@ -247,12 +217,10 @@ def to_json(self) -> dict: } -class VelpVersion(db.Model): - __tablename__ = "velpversion" - +class VelpVersion(DbModel): id: Mapped[int] = mapped_column(primary_key=True) - velp_id: Mapped[int] = mapped_column(db.ForeignKey("velp.id")) + velp_id: Mapped[int] = mapped_column(ForeignKey("velp.id")) modify_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) - velp: Mapped["Velp"] = db.relationship("Velp", overlaps="velp_versions") - content: Mapped[List["VelpContent"]] = db.relationship(overlaps="velp_version") + velp: Mapped["Velp"] = relationship("Velp", overlaps="velp_versions") + content: Mapped[List["VelpContent"]] = relationship(overlaps="velp_version") diff --git a/tim_common/timjsonencoder.py b/tim_common/timjsonencoder.py index 93d2b4e437..d95426bb51 100644 --- a/tim_common/timjsonencoder.py +++ b/tim_common/timjsonencoder.py @@ -12,6 +12,7 @@ try: from sqlalchemy.ext.declarative import DeclarativeMeta + from sqlalchemy.orm import DeclarativeBase sqlalchemy_imported = True except ImportError: @@ -46,7 +47,9 @@ def default(self, o): if tojson: return tojson() # from http://stackoverflow.com/a/31569287 with some changes - if sqlalchemy_imported and isinstance(o.__class__, DeclarativeMeta): + if sqlalchemy_imported and ( + isinstance(o, DeclarativeMeta) or isinstance(o, DeclarativeBase) + ): data = {} if hasattr(o, "__json__"): flds = o.__json__() From 57a279cc1808812464a6058247e7edc3d983508d Mon Sep 17 00:00:00 2001 From: dezhidki Date: Sat, 29 Jul 2023 16:35:43 +0300 Subject: [PATCH 12/34] Refactor db.session.execute -> run_sql This commit adds a run_sql helper which can be used to execute SQL queries. All current uses of basic SQL execution are replaces to use run_sql instead --- timApp/admin/answer_cli.py | 66 ++++++----- timApp/admin/associate_old_uploads.py | 17 +-- timApp/admin/change_group_email.py | 4 +- timApp/admin/fix_imagex_freehanddata.py | 13 +-- timApp/admin/fix_orphan_documents.py | 24 ++-- timApp/admin/item_cli.py | 38 +++--- timApp/admin/language_cli.py | 28 +++-- timApp/admin/routes.py | 22 ++-- timApp/admin/translationservice_cli.py | 4 +- timApp/admin/user_cli.py | 50 ++++---- timApp/answer/answers.py | 10 +- timApp/answer/backup.py | 4 +- timApp/answer/feedbackanswer.py | 4 +- timApp/answer/routes.py | 36 +++--- timApp/auth/access/routes.py | 12 +- timApp/auth/accesshelper.py | 24 ++-- timApp/auth/login.py | 16 +-- timApp/auth/oauth2/oauth2.py | 10 +- timApp/auth/session/routes.py | 12 +- timApp/auth/session/util.py | 14 +-- timApp/auth/sessioninfo.py | 4 +- timApp/backup/backup_routes.py | 7 +- timApp/bookmark/course.py | 22 ++-- timApp/document/changelog.py | 4 +- timApp/document/docentry.py | 12 +- timApp/document/docinfo.py | 6 +- timApp/document/documents.py | 10 +- timApp/document/editing/routes.py | 9 +- timApp/document/translation/deepl.py | 4 +- timApp/document/translation/language.py | 4 +- timApp/document/translation/routes.py | 109 +++++++++++++----- timApp/document/translation/translator.py | 6 +- timApp/folder/folder.py | 26 ++--- timApp/gamification/gamificationdata.py | 6 +- timApp/item/distribute_rights.py | 8 +- timApp/item/item.py | 4 +- timApp/item/manage.py | 18 ++- timApp/item/routes.py | 21 ++-- timApp/item/routes_tags.py | 10 +- timApp/item/taskblock.py | 12 +- timApp/lecture/askedjson.py | 8 +- timApp/lecture/askedquestion.py | 4 +- timApp/lecture/lecture.py | 8 +- timApp/lecture/lectureanswer.py | 4 +- timApp/lecture/routes.py | 18 ++- timApp/messaging/messagelist/emaillist.py | 6 +- .../messagelist/messagelist_models.py | 10 +- .../messagelist/messagelist_utils.py | 12 +- .../timMessage/internalmessage_models.py | 4 +- timApp/messaging/timMessage/routes.py | 24 ++-- timApp/note/notes.py | 6 +- timApp/note/routes.py | 6 +- timApp/notification/notify.py | 4 +- timApp/notification/pending_notification.py | 4 +- timApp/peerreview/util/groups.py | 10 +- timApp/peerreview/util/peerreview_utils.py | 22 ++-- timApp/plugin/calendar/calendar.py | 18 ++- timApp/plugin/calendar/models.py | 20 ++-- timApp/plugin/group_join/group_join.py | 8 +- timApp/plugin/importdata/importData.py | 10 +- timApp/plugin/jsrunner/util.py | 10 +- timApp/plugin/plugin.py | 6 +- timApp/plugin/pluginControl.py | 4 +- timApp/plugin/plugintype.py | 14 +-- timApp/plugin/tableform/tableForm.py | 4 +- timApp/plugin/userselect/action_queue.py | 10 +- timApp/plugin/userselect/userselect.py | 18 +-- timApp/printing/documentprinter.py | 4 +- timApp/readmark/readings.py | 16 ++- timApp/readmark/routes.py | 12 +- timApp/scheduling/scheduling_routes.py | 16 ++- timApp/sisu/scim.py | 8 +- timApp/sisu/sisu.py | 4 +- timApp/slide/routes.py | 6 +- timApp/tests/browser/test_questions.py | 4 +- timApp/tests/server/test_comments.py | 6 +- timApp/tests/server/test_default_rights.py | 8 +- timApp/tests/server/test_duration.py | 8 +- timApp/tests/server/test_notify.py | 6 +- timApp/tests/server/test_peer_review.py | 32 ++--- timApp/tests/server/test_personal_folder.py | 12 +- timApp/tests/server/test_plugins.py | 4 +- timApp/tests/server/test_self_expire.py | 6 +- timApp/tests/server/test_signup.py | 20 ++-- timApp/tests/server/test_tim_message.py | 10 +- timApp/tests/server/test_translation.py | 8 +- timApp/tests/server/test_user_sessions.py | 30 +++-- timApp/tests/server/test_velp.py | 40 ++----- timApp/tests/server/test_verification.py | 6 +- timApp/tests/server/timroutetest.py | 6 +- timApp/tim_celery.py | 6 +- timApp/timdb/sqa.py | 30 +++-- timApp/upload/upload.py | 6 +- timApp/upload/uploadedfile.py | 4 +- timApp/user/contacts.py | 8 +- timApp/user/groups.py | 10 +- timApp/user/hakaorganization.py | 4 +- timApp/user/personaluniquecode.py | 4 +- timApp/user/preferences.py | 6 +- timApp/user/settings/settings.py | 20 ++-- timApp/user/settings/styles.py | 6 +- timApp/user/user.py | 56 +++++---- timApp/user/usergroup.py | 28 ++--- timApp/user/users.py | 7 +- timApp/user/userutils.py | 6 +- timApp/user/verification/routes.py | 4 +- timApp/user/verification/verification.py | 6 +- timApp/util/flask/search.py | 6 +- timApp/util/get_fields.py | 18 ++- timApp/velp/annotation.py | 4 +- timApp/velp/annotations.py | 4 +- timApp/velp/velp.py | 30 +++-- timApp/velp/velpgroups.py | 32 +++-- timApp/velp/velps.py | 10 +- 114 files changed, 767 insertions(+), 802 deletions(-) diff --git a/timApp/admin/answer_cli.py b/timApp/admin/answer_cli.py index 294c034605..8eb3b5c95f 100644 --- a/timApp/admin/answer_cli.py +++ b/timApp/admin/answer_cli.py @@ -20,7 +20,7 @@ from timApp.item.block import Block from timApp.item.item import Item from timApp.plugin.taskid import TaskId -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.upload.uploadedfile import PluginUpload from timApp.user.user import User from timApp.user.usergroup import UserGroup @@ -39,7 +39,7 @@ @click.option("--dry-run/--no-dry-run", default=True) def fix_double_c(dry_run: bool) -> None: answers = ( - db.session.execute( + run_sql( select(Answer) .filter( (Answer.answered_on > datetime(year=2020, month=2, day=9)) @@ -79,9 +79,11 @@ class AnswerDeleteResult: @click.argument("doc", type=TimDocumentType()) @click.option("--dry-run/--no-dry-run", default=True) def clear_all(doc: DocInfo, dry_run: bool) -> None: - ids = db.session.scalars( - select(Answer.id).filter(Answer.task_id.startswith(f"{doc.id}.")) - ).all() + ids = ( + run_sql(select(Answer.id).filter(Answer.task_id.startswith(f"{doc.id}."))) + .scalars() + .all() + ) cnt = len(ids) delete_answers_with_ids(ids) @@ -110,7 +112,7 @@ def clear( stmt = stmt.filter(Answer.answered_on >= answer_from) if answer_to: stmt = stmt.filter(Answer.answered_on <= answer_to) - ids = db.session.scalars(stmt).all() + ids = run_sql(stmt).scalars().all() cnt = len(ids) result = delete_answers_with_ids(ids, verbose) click.echo(f"Total {cnt}") @@ -123,7 +125,7 @@ def delete_answers_with_ids( if not isinstance(ids, list): raise TypeError("ids should be a list of answer ids") d_ua = len( - db.session.execute( + run_sql( delete(UserAnswer) .where(UserAnswer.answer_id.in_(ids)) .returning(UserAnswer.id) @@ -132,7 +134,7 @@ def delete_answers_with_ids( ) d_as = len( ( - db.session.execute( + run_sql( delete(AnswerSaver) .where(AnswerSaver.answer_id.in_(ids)) .returning(AnswerSaver.user_id, AnswerSaver.answer_id) @@ -143,7 +145,7 @@ def delete_answers_with_ids( anns_stmt = select(Annotation.id).filter(Annotation.answer_id.in_(ids)) d_acs = len( ( - db.session.execute( + run_sql( delete(AnnotationComment) .where( AnnotationComment.annotation_id.in_( @@ -157,7 +159,7 @@ def delete_answers_with_ids( ) d_anns = len( ( - db.session.execute( + run_sql( delete(Annotation) .where(Annotation.id.in_(anns_stmt)) .returning(Annotation.id) @@ -171,12 +173,12 @@ def delete_answers_with_ids( "\n".join( [ f"taskid: {a.task_id}, points: {a.points}, answered_on: {a.answered_on}; saver: {a.saver}" - for a in db.session.scalars(ans_items_stmt) + for a in run_sql(ans_items_stmt).scalars() ] ) ) d_ans = len( - db.session.execute( + run_sql( delete(Answer) .where(Answer.id.in_(ans_items_stmt.with_only_columns(Answer.id))) .returning(Answer.id) @@ -201,13 +203,17 @@ def delete_answers_with_ids( def revalidate( doc: DocInfo, deadline: datetime, group: str, dry_run: bool, may_invalidate: bool ) -> None: - answers: list[tuple[Answer, str]] = db.session.scalars( - select(Answer, User.name) - .join(User, Answer.users) - .join(UserGroup, User.groups) - .filter(Answer.task_id.startswith(f"{doc.id}.")) - .order_by(Answer.answered_on.desc()) - ).all() + answers: list[tuple[Answer, str]] = ( + run_sql( + select(Answer, User.name) + .join(User, Answer.users) + .join(UserGroup, User.groups) + .filter(Answer.task_id.startswith(f"{doc.id}.")) + .order_by(Answer.answered_on.desc()) + ) + .scalars() + .all() + ) changed_to_valid = 0 changed_to_invalid = 0 @@ -244,7 +250,7 @@ def truncate_large(doc: DocInfo, limit: int, to: int, dry_run: bool) -> None: stmt = select(Answer).filter(Answer.task_id.startswith(f"{doc.id}.")) total = db.session.scalar(stmt.with_only_columns(func.count())) anss: list[Answer] = ( - db.session.execute( + run_sql( stmt.filter(func.length(Answer.content) > limit).options( selectinload(Answer.users_all) ) @@ -289,13 +295,17 @@ def truncate_large(doc: DocInfo, limit: int, to: int, dry_run: bool) -> None: def compress_uploads(item: Item, dry_run: bool) -> None: docs = collect_docs(item) for d in docs: - uploads: list[Block] = db.session.scalars( - select(Block) - .select_from(Answer) - .filter(Answer.task_id.startswith(f"{d.id}.")) - .join(AnswerUpload) - .join(Block) - ).all() + uploads: list[Block] = ( + run_sql( + select(Block) + .select_from(Answer) + .filter(Answer.task_id.startswith(f"{d.id}.")) + .join(AnswerUpload) + .join(Block) + ) + .scalars() + .all() + ) for u in uploads: path = u.description if path.lower().endswith(".pdf"): @@ -371,7 +381,7 @@ def delete_old_answers(d: DocInfo, tasks: list[str]) -> DeleteResult: todelete = base_query.filter(Answer.id.notin_(latest)).with_only_columns(Answer.id) tot = db.session.scalar(base_query.with_only_columns(func.count())) del_tot = db.session.scalar(todelete.with_only_columns(func.count())) - adr = delete_answers_with_ids(db.session.execute(todelete).scalars().all()) + adr = delete_answers_with_ids(run_sql(todelete).scalars().all()) r = DeleteResult(total=tot, deleted=del_tot, adr=adr) return r diff --git a/timApp/admin/associate_old_uploads.py b/timApp/admin/associate_old_uploads.py index 0b55052c72..dcdce9d963 100644 --- a/timApp/admin/associate_old_uploads.py +++ b/timApp/admin/associate_old_uploads.py @@ -11,7 +11,7 @@ from timApp.document.docinfo import DocInfo from timApp.item.block import BlockType, Block from timApp.item.blockassociation import BlockAssociation -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.upload.uploadedfile import UploadedFile from timApp.user.usergroup import UserGroup @@ -44,13 +44,16 @@ def del_anon(u: UploadedFile) -> None: SearchArgumentsBasic(format="", onlyfirst=False, regex=True, term=r), del_anon, ) - orphans = db.session.execute( - select(Block) - .filter( - Block.type_id.in_([BlockType.File.value, BlockType.Image.value]) - & Block.id.notin_(select(BlockAssociation.child)) + orphans = ( + run_sql( + select(Block).filter( + Block.type_id.in_([BlockType.File.value, BlockType.Image.value]) + & Block.id.notin_(select(BlockAssociation.child)) + ) ) - ).scalars().all() + .scalars() + .all() + ) print(f"Deleting anon accesses from {len(orphans)} orphan uploads") for o in orphans: del_anon(UploadedFile(o)) diff --git a/timApp/admin/change_group_email.py b/timApp/admin/change_group_email.py index 8e4dfbdb6d..5d02e93dc8 100644 --- a/timApp/admin/change_group_email.py +++ b/timApp/admin/change_group_email.py @@ -1,7 +1,7 @@ from sqlalchemy import select from timApp.tim_app import app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User, UserInfo from timApp.user.usergroup import UserGroup @@ -14,7 +14,7 @@ def change_email() -> None: # groupname = input("Input group to edit: ") groupname = "mallikurssinryhma1" group = ( - db.session.execute(select(UserGroup).filter_by(name=groupname).limit(1)) + run_sql(select(UserGroup).filter_by(name=groupname).limit(1)) .scalars() .first() ) diff --git a/timApp/admin/fix_imagex_freehanddata.py b/timApp/admin/fix_imagex_freehanddata.py index dd4eb42119..2feb96512f 100644 --- a/timApp/admin/fix_imagex_freehanddata.py +++ b/timApp/admin/fix_imagex_freehanddata.py @@ -5,7 +5,7 @@ from timApp.admin.util import process_items, create_argparser, DryrunnableArguments from timApp.answer.answer import Answer from timApp.document.docinfo import DocInfo -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql def fix_imagex_freehanddata(doc: DocInfo, args: DryrunnableArguments) -> int: @@ -18,12 +18,11 @@ def fix_imagex_freehanddata(doc: DocInfo, args: DryrunnableArguments) -> int: :param args: The arguments. """ errors = 0 - answers: list[Answer] = db.session.scalars( - select(Answer) - .filter( - Answer.task_id.startswith(f"{doc.id}.") - ) - ).all() + answers: list[Answer] = ( + run_sql(select(Answer).filter(Answer.task_id.startswith(f"{doc.id}."))) + .scalars() + .all() + ) for a in answers: data = a.content_as_json freehanddata = data.get("freeHandData") diff --git a/timApp/admin/fix_orphan_documents.py b/timApp/admin/fix_orphan_documents.py index a5fbcdd00c..7fa3f26f46 100644 --- a/timApp/admin/fix_orphan_documents.py +++ b/timApp/admin/fix_orphan_documents.py @@ -9,7 +9,7 @@ from timApp.folder.folder import Folder from timApp.item.block import Block, BlockType from timApp.timdb.dbaccess import get_files_path -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.usergroup import UserGroup @@ -18,13 +18,17 @@ def fix_orphans_without_docentry() -> None: creates a DocEntry for them under 'orphans' directory.""" orphan_folder_title = "orphans" f = Folder.create("orphans", UserGroup.get_admin_group()) - orphans: list[Block] = db.session.scalars( - select(Block).filter( - (Block.type_id == 0) - & Block.id.notin_(select(DocEntry.id)) - & Block.id.notin_(select(Translation.doc_id)) + orphans: list[Block] = ( + run_sql( + select(Block).filter( + (Block.type_id == 0) + & Block.id.notin_(select(DocEntry.id)) + & Block.id.notin_(select(Translation.doc_id)) + ) ) - ).all() + .scalars() + .all() + ) for o in orphans: print(f"Adding a DocEntry for document with id {o.id}") @@ -43,9 +47,9 @@ def move_docs_without_block(dry_run: bool) -> None: doc_folders = [f for f in os.listdir(docs_folder) if not isfile(f)] existing_blocks = { str(i) - for i, in db.session.scalars( - select(Block.id).filter_by(type_id=BlockType.Document.value) - ).all() + for i in run_sql(select(Block.id).filter_by(type_id=BlockType.Document.value)) + .scalars() + .all() } docs_orphans = os.path.join(files_root, "orphans", "docs") pars_orphans = os.path.join(files_root, "orphans", "pars") diff --git a/timApp/admin/item_cli.py b/timApp/admin/item_cli.py index 2f5a6bca2e..a14f33e651 100644 --- a/timApp/admin/item_cli.py +++ b/timApp/admin/item_cli.py @@ -17,7 +17,7 @@ from timApp.notification.pending_notification import PendingNotification from timApp.readmark.readparagraph import ReadParagraph from timApp.timdb.dbaccess import get_files_path -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.velp.velp_models import VelpGroupsInDocument @@ -26,19 +26,23 @@ @item_cli.command("cleanup_default_rights_names") def cleanup_default_right_doc_names() -> None: - bs: list[Block] = db.session.scalars( - select(Block).filter( - Block.description.in_( - [ - "templates/DefaultDocumentRights", - "templates/DefaultFolderRights", - "$DefaultFolderRights", - "$DefaultDocumentRights", - ] + bs: list[Block] = ( + run_sql( + select(Block).filter( + Block.description.in_( + [ + "templates/DefaultDocumentRights", + "templates/DefaultFolderRights", + "$DefaultFolderRights", + "$DefaultDocumentRights", + ] + ) + & (Block.type_id == BlockType.Document.value) ) - & (Block.type_id == BlockType.Document.value) ) - ).all() + .scalars() + .all() + ) num_changed = len(bs) for b in bs: b.description = b.description.replace("templates/", "").replace("$", "") @@ -53,9 +57,11 @@ def cleanup_default_right_doc_names() -> None: def cleanup_bookmark_docs( dry_run: bool, prompt_before_commit: bool, max_docs: int | None ) -> None: - new_bookmark_users: list[User] = db.session.scalars( - select(User).filter(User.prefs.contains('"bookmarks":')) - ).all() + new_bookmark_users: list[User] = ( + run_sql(select(User).filter(User.prefs.contains('"bookmarks":'))) + .scalars() + .all() + ) docs_to_delete = set() processed_users = 0 @@ -90,7 +96,7 @@ def cleanup_bookmark_docs( VelpGroupsInDocument, Notification, ): - db.session.execute(delete(t).where(t.doc_id == bm_doc.id)) + run_sql(delete(t).where(t.doc_id == bm_doc.id)) db.session.delete(bm_doc) db.session.delete(block) if dry_run: diff --git a/timApp/admin/language_cli.py b/timApp/admin/language_cli.py index 08ac706df9..06b00ba4d0 100644 --- a/timApp/admin/language_cli.py +++ b/timApp/admin/language_cli.py @@ -19,7 +19,7 @@ from timApp.document.translation.language import Language from timApp.tim_app import app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.util.logger import log_error, log_info, log_debug language_cli = AppGroup("language") @@ -35,9 +35,11 @@ def remove(lang_code: str) -> None: :return: None """ - exists: Language | None = db.session.scalars( - select(Language).filter(lang_code == Language.lang_code).limit(1) - ).first() + exists: Language | None = ( + run_sql(select(Language).filter(lang_code == Language.lang_code).limit(1)) + .scalars() + .first() + ) if exists: if click.confirm("This action cannot be reversed. Continue?"): click.echo( @@ -67,9 +69,11 @@ def add(lang_name: str) -> None: click.echo(f"Failed to create language: {str(e)}") return - exists: Language | None = db.session.scalars( - select(Language).filter(lang.lang_code == Language.lang_code).limit(1) - ).first() + exists: Language | None = ( + run_sql(select(Language).filter(lang.lang_code == Language.lang_code).limit(1)) + .scalars() + .first() + ) if exists: click.echo(f"Language code '{lang.lang_code}' already exists in the database.") else: @@ -99,7 +103,7 @@ def add_all_supported_languages(log: bool = False) -> None: :return: None. """ # Add to the database the languages found in config and skip existing ones. - langset = {x[0] for x in db.session.scalars(select(Language.lang_code))} + langset = {x[0] for x in run_sql(select(Language.lang_code)).scalars()} for l in app.config["LANGUAGES"]: if type(l) is dict: lang = Language( @@ -161,9 +165,11 @@ def create(langcode: str, langname: str, autonym: str, flag_uri: str) -> None: click.echo(f"Failed to create new language: {str(e)}") return - exists = db.session.scalars( - select(Language).filter(standard_code == Language.lang_code).limit(1) - ).first() + exists = ( + run_sql(select(Language).filter(standard_code == Language.lang_code).limit(1)) + .scalars() + .first() + ) if exists: click.echo(f"Language code '{standard_code}' already exists in the database.") else: diff --git a/timApp/admin/routes.py b/timApp/admin/routes.py index d89eab7428..21cdf4fc75 100644 --- a/timApp/admin/routes.py +++ b/timApp/admin/routes.py @@ -4,7 +4,7 @@ from sqlalchemy import select from timApp.auth.accesshelper import verify_admin -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.util.flask.responsehelper import safe_redirect, json_response @@ -46,13 +46,17 @@ def restart_server() -> Response: @admin_bp.get("/users/search/") def search_users(term: str, full: bool = False) -> Response: verify_admin() - result: list[User] = db.session.scalars( - select(User) - .filter( - User.name.ilike(f"%{term}%") - | User.real_name.ilike(f"%{term}%") - | User.email.ilike(f"%{term}%") + result: list[User] = ( + run_sql( + select(User) + .filter( + User.name.ilike(f"%{term}%") + | User.real_name.ilike(f"%{term}%") + | User.email.ilike(f"%{term}%") + ) + .order_by(User.id) ) - .order_by(User.id) - ).all() + .scalars() + .all() + ) return json_response([u.to_json(contacts=True, full=full) for u in result]) diff --git a/timApp/admin/translationservice_cli.py b/timApp/admin/translationservice_cli.py index 4c7cf4a0c1..254c579a63 100644 --- a/timApp/admin/translationservice_cli.py +++ b/timApp/admin/translationservice_cli.py @@ -18,7 +18,7 @@ from timApp.document.translation.translator import TranslationService from timApp.tim_app import app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql tr_service_cli = AppGroup("trservice") @@ -52,7 +52,7 @@ def add_all_tr_services_to_session(log: bool = False) -> None: :return: None. """ existing_services = { - x[0] for x in db.session.scalars(select(TranslationService.service_name)) + x[0] for x in run_sql(select(TranslationService.service_name)).scalars() } for translator, init_data in app.config["MACHINE_TRANSLATORS"]: service_name = translator.__mapper_args__["polymorphic_identity"] diff --git a/timApp/admin/user_cli.py b/timApp/admin/user_cli.py index 8dec39635d..4ff4f8e8f7 100644 --- a/timApp/admin/user_cli.py +++ b/timApp/admin/user_cli.py @@ -18,7 +18,7 @@ from timApp.document.docentry import DocEntry from timApp.document.docinfo import move_document from timApp.tim_app import get_home_organization_group -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.personaluniquecode import SchacPersonalUniqueCode, PersonalUniqueCode from timApp.user.user import User, UserInfo, deleted_user_suffix from timApp.user.usercontact import UserContact @@ -93,9 +93,9 @@ def migrate_themes_to_styles(dry_run: bool, skip_warnings: bool) -> None: click.echo("Updating user styles") - for u in db.session.scalars( + for u in run_sql( select(User).filter(User.prefs != None) - ): # type: User + ).scalars(): # type: User prefs_json: dict = json.loads(u.prefs) css_combined = prefs_json.pop("css_combined", None) css_files: dict[str, bool] = prefs_json.pop("css_files", {}) @@ -336,7 +336,7 @@ def create( ) -> None: """Creates or updates a user.""" - user = db.session.scalars(select(User).filter_by(name=username).limit(1)).first() + user = run_sql(select(User).filter_by(name=username).limit(1)).scalars().first() info = UserInfo( username=username, email=email or None, @@ -390,9 +390,11 @@ def create_mass_users( return for i in range(lowerlimit, higherlimit + 1): strnum = str(i) - user = db.session.scalars( - select(User).filter_by(name=username + strnum).limit(1) - ).first() + user = ( + run_sql(select(User).filter_by(name=username + strnum).limit(1)) + .scalars() + .first() + ) # print(i) info = UserInfo( username=username + strnum, @@ -413,12 +415,16 @@ def create_mass_users( @user_cli.command() def fix_aalto_student_ids() -> None: - users_to_fix: list[User] = db.session.scalars( - select(User) - .select_from(UserGroup) - .filter(UserGroup.name.in_(["aalto19test", "cs-a1141-2017-2018"])) - .join(User, UserGroup.users) - ).all() + users_to_fix: list[User] = ( + run_sql( + select(User) + .select_from(UserGroup) + .filter(UserGroup.name.in_(["aalto19test", "cs-a1141-2017-2018"])) + .join(User, UserGroup.users) + ) + .scalars() + .all() + ) for u in users_to_fix: u.set_unique_codes( [ @@ -551,15 +557,19 @@ def find_duplicate_accounts() -> None: def find_duplicate_accounts_by_email() -> list[tuple[User, set[User]]]: email_lwr = func.lower(User.email) - dupes: list[User] = db.session.scalars( - select(User) - .filter( - email_lwr.in_( - select(email_lwr).group_by(email_lwr).having(func.count("*") > 1) + dupes: list[User] = ( + run_sql( + select(User) + .filter( + email_lwr.in_( + select(email_lwr).group_by(email_lwr).having(func.count("*") > 1) + ) ) + .order_by(User.email) ) - .order_by(User.email) - ).all() + .scalars() + .all() + ) result = [] dupegroups = [ list(g) for _, g in (itertools.groupby(dupes, lambda u: u.email.lower())) diff --git a/timApp/answer/answers.py b/timApp/answer/answers.py index dd80bbadda..ca6170ba76 100644 --- a/timApp/answer/answers.py +++ b/timApp/answer/answers.py @@ -31,7 +31,7 @@ from timApp.document.viewcontext import OriginInfo from timApp.plugin.plugintype import PluginType, PluginTypeLazy, PluginTypeBase from timApp.plugin.taskid import TaskId -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.upload.upload import get_pluginupload from timApp.user.user import Consent, User from timApp.user.usergroup import UserGroup @@ -333,7 +333,7 @@ def get_all_answers( if options.print == AnswerPrintOptions.ANSWERS_NO_LINE: lf = "" - qq: Iterable[tuple[Answer, User, int]] = db.session.execute(stmt) + qq: Iterable[tuple[Answer, User, int]] = run_sql(stmt) cnt = 0 hidden_user_names: dict[str, str] = {} @@ -500,7 +500,7 @@ def get_existing_answers_info( users: list[User], task_id: TaskId, only_valid: bool ) -> ExistingAnswersInfo: stmt = get_answers_query(task_id, users, only_valid) - latest = db.session.execute(stmt.limit(1)).scalars().first() + latest = run_sql(stmt.limit(1)).scalars().first() count = db.session.scalar(select(func.count()).select_from(stmt.subquery())) return ExistingAnswersInfo(latest_answer=latest, count=count) @@ -701,7 +701,7 @@ def get_users_for_tasks( ).order_by(User.real_name) def g() -> Generator[UserTaskEntry, None, None]: - for r in db.session.execute(main_stmt): + for r in run_sql(main_stmt): d = r._asdict() d["user"] = d.pop("User") task = d["task_points"] @@ -1111,4 +1111,4 @@ def get_global_answers(parsed_task_ids: dict[str, TaskId]) -> list[Answer]: .subquery() ) global_datas = select(Answer).join(sq2, Answer.id == sq2.c.aid) - return db.session.execute(global_datas).scalars().all() + return run_sql(global_datas).scalars().all() diff --git a/timApp/answer/backup.py b/timApp/answer/backup.py index 1dc97680cb..f0998e10f4 100644 --- a/timApp/answer/backup.py +++ b/timApp/answer/backup.py @@ -7,7 +7,7 @@ from timApp.answer.answer import Answer from timApp.answer.exportedanswer import ExportedAnswer from timApp.document.docentry import DocEntry -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.user.usergroupmember import UserGroupMember, membership_current @@ -70,7 +70,7 @@ def sync_user_group_memberships_if_enabled(user: User) -> None: user_groups: list[str] = [ ugn for ugn, in ( - db.session.execute( + run_sql( select(UserGroup.name) .join( UserGroupMember, diff --git a/timApp/answer/feedbackanswer.py b/timApp/answer/feedbackanswer.py index c7edf80fa0..14a33765d2 100644 --- a/timApp/answer/feedbackanswer.py +++ b/timApp/answer/feedbackanswer.py @@ -14,7 +14,7 @@ from timApp.document.viewcontext import default_view_ctx from timApp.plugin.plugin import Plugin, find_task_ids from timApp.plugin.taskid import TaskId -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.user import User from timApp.util.answerutil import get_answer_period, AnswerPeriodOptions from timApp.util.flask.requesthelper import get_option @@ -65,7 +65,7 @@ def get_all_feedback_answers( q = q.with_only_columns(Answer, User) # Makes q query an iterable qq for for-loop. - qq: Iterable[tuple[Answer, User]] = db.session.execute(q) + qq: Iterable[tuple[Answer, User]] = run_sql(q) return compile_csv(qq, printname, hide_names, exp_answers, users, dec) diff --git a/timApp/answer/routes.py b/timApp/answer/routes.py index 7163d18ce9..217e7cfdf1 100644 --- a/timApp/answer/routes.py +++ b/timApp/answer/routes.py @@ -113,7 +113,7 @@ from timApp.plugin.plugintype import PluginTypeBase from timApp.plugin.taskid import TaskId, TaskIdAccess from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.groups import ( verify_group_view_access, ) @@ -188,7 +188,7 @@ def save_review_points( if not is_peerreview_enabled(doc): raise AccessDenied("Peer review is not enabled") peer_review = ( - db.session.execute( + run_sql( select(PeerReview) .filter_by( block_id=tid.doc_id, @@ -448,9 +448,7 @@ def get_useranswers_for_task( .subquery() ) answs: list[Answer] = ( - db.session.execute(select(Answer).join(sub, Answer.id == sub.c.col).options(selectinload(Answer.users_all))) - .scalars() - .all() + run_sql(select(Answer).join(sub, Answer.id == sub.c.col).options(selectinload(Answer.users_all))).scalars().all() ) for answer in answs: asd = answer.to_json() @@ -474,7 +472,7 @@ def get_globals_for_tasks(task_ids: list[TaskId], answer_map: dict[str, dict]) - .join(sub, Answer.id == sub.c.col) .with_only_columns(Answer, sub.c.cnt) ) - for answer, _ in db.session.execute(answers_all): + for answer, _ in run_sql(answers_all): asd = answer.to_json() answer_map[answer.task_id] = asd @@ -1289,7 +1287,7 @@ def check_answerupload_file_accesses( uploads: list[AnswerUpload] = [] doc_map = {} blocks = ( - db.session.execute( + run_sql( select(Block).filter( Block.description.in_(filelist) & (Block.type_id == BlockType.Upload.value) @@ -1584,7 +1582,7 @@ def export_answers(doc_path: str) -> Response: if not d: raise RouteException("Document not found") verify_teacher_access(d) - answer_list: list[tuple[Answer, str]] = db.session.execute( + answer_list: list[tuple[Answer, str]] = run_sql( select(Answer) .filter(Answer.task_id.startswith(f"{d.id}.")) .join(User, Answer.users) @@ -1624,9 +1622,7 @@ def import_answers( verify_group_view_access(ug) doc_paths = {doc_map.get(a.doc, a.doc) for a in exported_answers} docs = ( - db.session.execute(select(DocEntry).filter(DocEntry.name.in_(doc_paths))) - .scalars() - .all() + run_sql(select(DocEntry).filter(DocEntry.name.in_(doc_paths))).scalars().all() ) doc_path_map = {d.path: d for d in docs} missing_docs = doc_paths - set(doc_path_map) @@ -1652,7 +1648,7 @@ def import_answers( f"Found: {seq_to_str([str((a.email, a.username)) for a in mixed_answers])}" ) - existing_answers: list[tuple[Answer, str]] = db.session.execute( + existing_answers: list[tuple[Answer, str]] = run_sql( select(Answer) .filter(filter_cond) .join(User, Answer.users) @@ -1681,7 +1677,7 @@ def convert_email_case(email: str | None) -> str | None: dupes = 0 # noinspection PyUnresolvedReferences all_users = ( - db.session.execute( + run_sql( select(User).filter( email_field.in_([a.email for a in exported_answers if a.email]) | name_field.in_([a.username for a in exported_answers if a.username]) @@ -1805,7 +1801,7 @@ def get_answers(task_id: str, user_id: int) -> Response: verify_view_access(d) user_context = user_context_with_logged_in(curr_user) user_answers = ( - db.session.execute( + run_sql( select(Answer) .filter_by(task_id=tid.doc_task) .order_by(Answer.id.desc()) @@ -2211,7 +2207,7 @@ def get_task_users(task_id: str, peer_review: bool = False) -> Response: stmt = stmt.join(UserGroup, User.groups).filter( UserGroup.name.in_(usergroups) ) - users = db.session.execute(stmt).scalars().all() + users = run_sql(stmt).scalars().all() if hide_names_in_teacher(d): model_u = User.get_model_answer_user() for user in users: @@ -2237,9 +2233,7 @@ def rename_answers(old_name: str, new_name: str, doc_path: str) -> Response: f"The new name conflicts with {conflicts} other answers with the same task name." ) answers_to_rename = ( - db.session.execute(select(Answer).filter_by(task_id=f"{d.id}.{old_name}")) - .scalars() - .all() + run_sql(select(Answer).filter_by(task_id=f"{d.id}.{old_name}")).scalars().all() ) for a in answers_to_rename: a.task_id = f"{d.id}.{new_name}" @@ -2265,7 +2259,7 @@ def clear_task_block(user: str, task_id: str) -> Response: if not b: return json_response({"cleared": False}) ba = ( - db.session.execute( + run_sql( select(BlockAccess) .filter_by( block_id=b.id, @@ -2316,7 +2310,7 @@ def unlock_locked_task(task_id: str) -> Response: b = TaskBlock.get_by_task(prerequisite_taskid.doc_task) if b: ba = ( - db.session.execute( + run_sql( select(BlockAccess) .filter_by( block_id=b.id, @@ -2370,7 +2364,7 @@ def unlock_task(task_id: str) -> Response: b = insert_task_block(task_id=tid.doc_task, owner_groups=d.owners) else: ba = ( - db.session.execute( + run_sql( select(BlockAccess) .filter_by( block_id=b.id, diff --git a/timApp/auth/access/routes.py b/timApp/auth/access/routes.py index 822840f522..66f1b32552 100644 --- a/timApp/auth/access/routes.py +++ b/timApp/auth/access/routes.py @@ -11,7 +11,7 @@ from timApp.auth.accesshelper import verify_logged_in, AccessDenied from timApp.auth.accesstype import AccessType from timApp.auth.sessioninfo import get_current_user_object -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.groups import ( verify_group_edit_access, get_group_or_abort, @@ -72,9 +72,7 @@ def lock_active_groups(group_ids: list[int] | None) -> Response: if not user.is_admin: groups: list[UserGroup] = ( - db.session.execute( - select(UserGroup).filter(UserGroup.id.in_(group_ids_set)) - ) + run_sql(select(UserGroup).filter(UserGroup.id.in_(group_ids_set))) .scalars() .all() ) @@ -126,11 +124,7 @@ def find_editable_groups( verify_logged_in() user = get_current_user_object() user.bypass_access_lock = True - ugs = ( - db.session.execute(select(UserGroup).filter(UserGroup.id.in_(group_ids))) - .scalars() - .all() - ) + ugs = run_sql(select(UserGroup).filter(UserGroup.id.in_(group_ids))).scalars().all() visible_ugs = [ ug for ug in ugs if user.is_admin or verify_group_edit_access(ug, require=False) ] diff --git a/timApp/auth/accesshelper.py b/timApp/auth/accesshelper.py index 6fa4b10797..6c6e0e7c12 100644 --- a/timApp/auth/accesshelper.py +++ b/timApp/auth/accesshelper.py @@ -43,7 +43,7 @@ from timApp.plugin.pluginexception import PluginException from timApp.plugin.taskid import TaskId, TaskIdAccess from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import ItemOrBlock, User from timApp.user.usergroup import UserGroup from timApp.user.userutils import grant_access @@ -287,18 +287,22 @@ def abort_if_not_access_and_required( block_ids = [block.id, *get_inherited_right_blocks(block)] if check_duration: - ba = db.session.scalars( - select(BlockAccess) - .filter(BlockAccess.block_id.in_(block_ids)) - .filter_by( - type=access_type.value, - usergroup_id=get_current_user_group(), + ba = ( + run_sql( + select(BlockAccess) + .filter(BlockAccess.block_id.in_(block_ids)) + .filter_by( + type=access_type.value, + usergroup_id=get_current_user_group(), + ) + .limit(1) ) - .limit(1) - ).first() + .scalars() + .first() + ) if ba is None: ba_group: BlockAccess = ( - db.session.execute( + run_sql( select(BlockAccess) .filter(BlockAccess.block_id.in_(block_ids)) .filter_by(type=access_type.value) diff --git a/timApp/auth/login.py b/timApp/auth/login.py index fdc08187b3..f8ad0ef793 100644 --- a/timApp/auth/login.py +++ b/timApp/auth/login.py @@ -29,7 +29,7 @@ ) from timApp.notification.send_email import send_email from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.newuser import NewUser from timApp.user.personaluniquecode import PersonalUniqueCode from timApp.user.user import User, UserOrigin, UserInfo @@ -267,11 +267,7 @@ def do_email_signup_or_password_reset( password_hash = create_password_hash(password) new_password = True if not is_simple_email_login_enabled(): - nu = ( - db.session.execute(select(NewUser).filter_by(email=email).limit(1)) - .scalars() - .first() - ) + nu = run_sql(select(NewUser).filter_by(email=email).limit(1)).scalars().first() if nu: nu.pass_ = password_hash new_password = False @@ -318,9 +314,7 @@ def check_temp_pw(email_or_username: str, oldpass: str) -> NewUser: else: name_filter = [email_or_username] valid_nu = None - for nu in db.session.execute( - select(NewUser).filter(NewUser.email.in_(name_filter)) - ).scalars(): + for nu in run_sql(select(NewUser).filter(NewUser.email.in_(name_filter))).scalars(): if nu.check_password(oldpass): valid_nu = nu if not valid_nu: @@ -402,7 +396,7 @@ def email_signup_finish( ) db.session.flush() - db.session.execute( + run_sql( delete(NewUser) .where(NewUser.email.in_((user.name, user.email))) .execution_options(synchronize_session=False) @@ -585,7 +579,7 @@ def quick_login(username: str) -> Response: ) ) if not ( - db.session.execute(stmt.limit(1)).scalars().first() + run_sql(stmt.limit(1)).scalars().first() and not check_admin_access(user=user) ): raise AccessDenied("Sorry, you don't have permission to quickLogin.") diff --git a/timApp/auth/oauth2/oauth2.py b/timApp/auth/oauth2/oauth2.py index 24a13d7a27..d247c9fbf0 100644 --- a/timApp/auth/oauth2/oauth2.py +++ b/timApp/auth/oauth2/oauth2.py @@ -11,7 +11,7 @@ from sqlalchemy import select, delete from timApp.auth.oauth2.models import OAuth2Client, OAuth2Token, OAuth2AuthorizationCode -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from tim_common.marshmallow_dataclass import class_schema @@ -27,9 +27,7 @@ class RefreshTokenGrant(grants.RefreshTokenGrant): def authenticate_refresh_token(self, refresh_token: str) -> OAuth2Token | None: token: OAuth2Token = ( - db.session.execute( - select(OAuth2Token).filter_by(refresh_token=refresh_token).limit(1) - ) + run_sql(select(OAuth2Token).filter_by(refresh_token=refresh_token).limit(1)) .scalars() .first() ) @@ -71,7 +69,7 @@ def query_authorization_code( self, code: str, client: OAuth2Client ) -> OAuth2AuthorizationCode | None: auth_code = ( - db.session.execute( + run_sql( select(OAuth2AuthorizationCode).filter_by( code=code, client_id=client.client_id ) @@ -108,7 +106,7 @@ def query_client(client_id: str) -> OAuth2Client: def delete_expired_oauth2_tokens() -> None: now_time = int(time.time()) - db.session.execute( + run_sql( delete(OAuth2Token).where( (OAuth2Token.expires_in + OAuth2Token.issued_at < now_time) | (OAuth2Token.access_token_revoked_at < now_time) diff --git a/timApp/auth/session/routes.py b/timApp/auth/session/routes.py index ef7da2ed71..d9c42dfbb4 100644 --- a/timApp/auth/session/routes.py +++ b/timApp/auth/session/routes.py @@ -19,7 +19,7 @@ invalidate_sessions_for, ) from timApp.tim_app import csrf -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.util.flask.requesthelper import RouteException from timApp.util.flask.responsehelper import json_response, csv_response, ok_response @@ -111,12 +111,12 @@ def get_all_sessions( match export_format: case ExportFormatOptions.JSON: - return json_response(db.session.execute(stmt).scalars().all()) + return json_response(run_sql(stmt).scalars().all()) case ExportFormatOptions.CSV: data: list[list[Any]] = [ ["user", "session_id", "origin", "logged_in_at", "expired_at"] ] - for s in db.session.execute(stmt).scalars().all(): # type: UserSession + for s in run_sql(stmt).scalars().all(): # type: UserSession data.append( [ s.user.name, @@ -204,9 +204,7 @@ def validate_all() -> Response: verify_admin() all_usersnames: list[tuple[str]] = ( - db.session.execute( - select(User.name).join(UserSession).distinct(UserSession.user_id) - ) + run_sql(select(User.name).join(UserSession).distinct(UserSession.user_id)) .scalars() .all() ) @@ -224,7 +222,7 @@ def invalidate_all() -> Response: """ verify_admin() - db.session.execute( + run_sql( update(UserSession) .where(~UserSession.expired) .values({"expired_at": get_current_time()}) diff --git a/timApp/auth/session/util.py b/timApp/auth/session/util.py index 43779d1554..77c0c0fec4 100644 --- a/timApp/auth/session/util.py +++ b/timApp/auth/session/util.py @@ -9,7 +9,7 @@ from timApp.auth.sessioninfo import get_current_user_object from timApp.item.item import Item from timApp.tim_app import app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User, ItemOrBlock from timApp.user.userutils import get_anon_user_id from timApp.util.logger import log_info, log_warning @@ -70,9 +70,7 @@ def expire_user_session(user: User, session_id: str | None) -> None: if not _save_sessions() or not session_id: return sess = ( - db.session.execute( - select(UserSession).filter_by(user=user, session_id=session_id) - ) + run_sql(select(UserSession).filter_by(user=user, session_id=session_id)) .scalars() .first() ) @@ -159,7 +157,7 @@ def has_valid_session(user: User | None = None) -> bool: return False current_session = ( - db.session.execute( + run_sql( select(UserSession.session_id) .filter( (UserSession.user == user) @@ -206,8 +204,8 @@ def verify_session_for(username: str, session_id: str | None = None) -> None: # Only expire active sessions stmt_expire = stmt_expire.where(UserSession.expired == False) - db.session.execute(stmt_expire.values({"expired_at": get_current_time()})) - db.session.execute(stmt_verify.values({"expired_at": None})) + run_sql(stmt_expire.values({"expired_at": get_current_time()})) + run_sql(stmt_verify.values({"expired_at": None})) def invalidate_sessions_for(username: str, session_id: str | None = None) -> None: @@ -227,7 +225,7 @@ def invalidate_sessions_for(username: str, session_id: str | None = None) -> Non if session_id: stmt_invalidate = stmt_invalidate.filter(UserSession.session_id == session_id) - db.session.execute(stmt_invalidate) + run_sql(stmt_invalidate) def distribute_session_verification( diff --git a/timApp/auth/sessioninfo.py b/timApp/auth/sessioninfo.py index 4ad4d840c6..9d2719cd35 100644 --- a/timApp/auth/sessioninfo.py +++ b/timApp/auth/sessioninfo.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import joinedload from timApp.document.usercontext import UserContext -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import ( User, ) @@ -83,7 +83,7 @@ def get_other_session_users_objs() -> list[User]: def get_users_objs(lis) -> list[User]: return ( - db.session.execute(select(User).filter(User.id.in_([u["id"] for u in lis]))) + run_sql(select(User).filter(User.id.in_([u["id"] for u in lis]))) .scalars() .all() ) diff --git a/timApp/backup/backup_routes.py b/timApp/backup/backup_routes.py index 5502424404..3b7f3dbcaf 100644 --- a/timApp/backup/backup_routes.py +++ b/timApp/backup/backup_routes.py @@ -4,7 +4,7 @@ from timApp.answer.backup import save_answer_backup from timApp.answer.exportedanswer import ExportedAnswer from timApp.tim_app import csrf -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.user.usergroupmember import UserGroupMember, membership_current @@ -50,8 +50,9 @@ def receive_user_memberships( user.add_to_group(ug, None) if removed_memberships: - removed_memberships_objs: list[UserGroupMember] = db.session.execute( - select(UserGroupMember).join(UserGroup, UserGroupMember.group) + removed_memberships_objs: list[UserGroupMember] = run_sql( + select(UserGroupMember) + .join(UserGroup, UserGroupMember.group) .join(User, UserGroupMember.user) .filter( (User.name == user.name) diff --git a/timApp/bookmark/course.py b/timApp/bookmark/course.py index b291b7b50f..6834732282 100644 --- a/timApp/bookmark/course.py +++ b/timApp/bookmark/course.py @@ -7,7 +7,7 @@ from timApp.document.docinfo import DocInfo from timApp.item.block import Block from timApp.item.tag import Tag, GROUP_TAG_PREFIX -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.usergroup import UserGroup from timApp.util.utils import get_current_time @@ -17,15 +17,19 @@ def update_user_course_bookmarks() -> None: now = get_current_time() for gr in u.groups: # type: UserGroup if gr.is_sisu_student_group or gr.is_self_join_course: - docs = db.session.execute( - select(DocEntry).join(Block) - .join(Tag) - .filter( - (Tag.name == GROUP_TAG_PREFIX + gr.name) - & ((Tag.expires == None) | (Tag.expires > now)) + docs = ( + run_sql( + select(DocEntry) + .join(Block) + .join(Tag) + .filter( + (Tag.name == GROUP_TAG_PREFIX + gr.name) + & ((Tag.expires == None) | (Tag.expires > now)) + ) ) - - ).scalars().all() + .scalars() + .all() + ) if not docs: continue if len(docs) > 1: diff --git a/timApp/document/changelog.py b/timApp/document/changelog.py index 3a50c50ac8..e002c41574 100644 --- a/timApp/document/changelog.py +++ b/timApp/document/changelog.py @@ -6,7 +6,7 @@ import timApp from timApp.document.changelogentry import ChangelogEntry from timApp.document.docparagraph import DocParagraph -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql if TYPE_CHECKING: from timApp.user.user import User @@ -64,7 +64,7 @@ def get_authorinfo(self, pars: list[DocParagraph]) -> dict[str, AuthorInfo]: par_entry_map[e.par_id][e.group_id].append(e) User = timApp.user.user.User UserGroup = timApp.user.usergroup.UserGroup - result = db.session.execute( + result = run_sql( select(UserGroup, User) .select_from(UserGroup) .filter(UserGroup.id.in_(usergroup_ids)) diff --git a/timApp/document/docentry.py b/timApp/document/docentry.py index dac4f76f47..377522f236 100644 --- a/timApp/document/docentry.py +++ b/timApp/document/docentry.py @@ -12,7 +12,7 @@ from timApp.item.block import BlockType, Block from timApp.item.block import insert_block from timApp.timdb.exceptions import ItemAlreadyExistsException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup, get_admin_group_id from timApp.util.utils import split_location @@ -93,11 +93,11 @@ def translations(self) -> list[Translation]: @staticmethod def get_all() -> list[DocEntry]: - return db.session.execute(select(DocEntry)).scalars().all() + return run_sql(select(DocEntry)).scalars().all() @staticmethod def find_all_by_id(doc_id: int) -> list[DocEntry]: - return db.session.execute(select(DocEntry).filter_by(id=doc_id)).scalars().all() + return run_sql(select(DocEntry).filter_by(id=doc_id)).scalars().all() @staticmethod def find_by_id(doc_id: int, docentry_load_opts: Any = None) -> DocInfo | None: @@ -108,7 +108,7 @@ def find_by_id(doc_id: int, docentry_load_opts: Any = None) -> DocInfo | None: stmt = select(DocEntry).filter_by(id=doc_id) if docentry_load_opts: stmt = stmt.options(*docentry_load_opts) - d = db.session.execute(stmt.limit(1)).scalars().first() + d = run_sql(stmt.limit(1)).scalars().first() if d: return d return db.session.get(Translation, doc_id) @@ -141,7 +141,7 @@ def find_by_path( # Match lang id using LIKE to allow for partial matches. # This is a simple way to allow mapping /en to newer /en-US or /en-GB. tr = ( - db.session.execute( + run_sql( select(Translation).filter( (Translation.src_docid == entry.id) & (Translation.lang_id.like(f"{lang}%")) @@ -268,7 +268,7 @@ def get_documents( stmt = stmt.filter(custom_filter) if query_options is not None: stmt = stmt.options(query_options) - result = db.session.execute(stmt).scalars().all() + result = run_sql(stmt).scalars().all() if not filter_user: return result return [r for r in result if filter_user.has_view_access(r)] diff --git a/timApp/document/docinfo.py b/timApp/document/docinfo.py index 7ddc3ed2a4..22745ed0b8 100644 --- a/timApp/document/docinfo.py +++ b/timApp/document/docinfo.py @@ -17,7 +17,7 @@ from timApp.item.item import Item from timApp.markdown.markdownconverter import expand_macros_info from timApp.notification.notification import Notification -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.util.utils import get_current_time, partition from tim_common.utils import safe_parse_item_list @@ -175,7 +175,7 @@ def absolute_path(variable: str) -> bool: from timApp.document.translation.translation import Translation def get_docs(doc_paths: list[str]) -> list[tuple[DocEntry, Translation | None]]: - return db.session.execute( + return run_sql( select(DocEntry, Translation) .select_from(DocEntry) .filter(DocEntry.name.in_(doc_paths)) @@ -250,7 +250,7 @@ def get_notifications(self, condition) -> list[Notification]: .filter(Notification.block_id.in_([f.id for f in items])) ) stmt = stmt.filter(condition) - return db.session.execute(stmt).scalars().all() + return run_sql(stmt).scalars().all() def has_translation(self, lang_id): for t in self.translations: diff --git a/timApp/document/documents.py b/timApp/document/documents.py index 7fe372e78e..bed35eadf6 100644 --- a/timApp/document/documents.py +++ b/timApp/document/documents.py @@ -12,7 +12,7 @@ from timApp.item.block import Block, BlockType from timApp.note.usernote import UserNote from timApp.readmark.readparagraph import ReadParagraph -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.usergroup import UserGroup @@ -142,14 +142,16 @@ def delete_document(document_id: int): for stmt in ( delete(DocEntry).where(DocEntry.id == document_id), delete(BlockAccess).where(BlockAccess.block_id == document_id), - delete(Block).where((Block.type_id == BlockType.Document.value) & (Block.id == document_id)), + delete(Block).where( + (Block.type_id == BlockType.Document.value) & (Block.id == document_id) + ), delete(ReadParagraph).where(ReadParagraph.doc_id == document_id), delete(UserNote).where(UserNote.doc_id == document_id), delete(Translation).where( (Translation.doc_id == document_id) | (Translation.src_docid == document_id) - ) + ), ): - db.session.execute(stmt) + run_sql(stmt) Document.remove(document_id) diff --git a/timApp/document/editing/routes.py b/timApp/document/editing/routes.py index db32604208..3e84a497a1 100644 --- a/timApp/document/editing/routes.py +++ b/timApp/document/editing/routes.py @@ -56,9 +56,10 @@ from timApp.plugin.qst.qst import question_convert_js_to_yaml from timApp.plugin.save_plugin import save_plugin from timApp.readmark.readings import mark_read + # from timApp.timdb.dbaccess import get_timdb from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.upload.uploadedfile import UploadedFile from timApp.util.flask.requesthelper import ( verify_json_params, @@ -763,7 +764,11 @@ def check_duplicates(pars, doc): duplicate.append(task_id) duplicate.append(par.get_id()) task_id_to_check = str(doc.doc_id) + "." + task_id - if db.session.execute(select(Answer).filter_by(task_id=task_id_to_check)).scalars().first(): + if ( + run_sql(select(Answer).filter_by(task_id=task_id_to_check)) + .scalars() + .first() + ): duplicate.append("hasAnswers") duplicates.append(duplicate) break diff --git a/timApp/document/translation/deepl.py b/timApp/document/translation/deepl.py index 56a08028a0..a0bcb96c3a 100644 --- a/timApp/document/translation/deepl.py +++ b/timApp/document/translation/deepl.py @@ -33,7 +33,7 @@ LanguagePairing, replace_md_aliases, ) -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.usergroup import UserGroup from timApp.util import logger from timApp.util.flask.cache import cache @@ -86,7 +86,7 @@ def register(self, user_group: UserGroup) -> None: """ # One user group should match one service per one key. api_key = ( - db.session.execute( + run_sql( select(TranslationServiceKey).filter( TranslationServiceKey.service_id == self.id, TranslationServiceKey.group_id == user_group.id, diff --git a/timApp/document/translation/language.py b/timApp/document/translation/language.py index fc3a94fea8..6134d129d4 100644 --- a/timApp/document/translation/language.py +++ b/timApp/document/translation/language.py @@ -19,7 +19,7 @@ from sqlalchemy import select from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import DbModel @@ -100,7 +100,7 @@ def query_all(cls) -> list["Language"]: :return: All the languages found from database. """ - return db.session.execute(select(cls)).scalars().all() + return run_sql(select(cls)).scalars().all() def __str__(self) -> str: """ diff --git a/timApp/document/translation/routes.py b/timApp/document/translation/routes.py index 1f9869c9ff..306af5e317 100644 --- a/timApp/document/translation/routes.py +++ b/timApp/document/translation/routes.py @@ -49,7 +49,7 @@ from timApp.item.copy_rights import copy_rights from timApp.item.routes import get_document_relevance, set_relevance from timApp.timdb.exceptions import ItemAlreadyExistsException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.util.flask.requesthelper import verify_json_params, NotExist, RouteException from timApp.util.flask.responsehelper import json_response, ok_response, Response @@ -153,10 +153,15 @@ def get_languages(source_languages: bool) -> Response: else: # Get the translation service by the provided service name # TODO Maybe change to use an id instead? - tr = db.session.execute( - select(TranslationService).with_polymorphic("*") - .filter(TranslationService.service_name == translator) - ).scalars().one() + tr = ( + run_sql( + select(TranslationService) + .with_polymorphic("*") + .filter(TranslationService.service_name == translator) + ) + .scalars() + .one() + ) if isinstance(tr, RegisteredTranslationService): tr.register(get_current_user_object().get_personal_group()) @@ -399,7 +404,7 @@ def get_all_languages() -> Response: :return: JSON response containing all the available languages. """ - langs = sorted(db.session.execute(select(Language)).scalars().all()) + langs = sorted(run_sql(select(Language)).scalars().all()) return json_response(langs) @@ -422,7 +427,9 @@ def get_translators() -> Response: :return: JSON response containing the translators. """ - translationservice_names = db.session.execute(select(TranslationService.service_name)).scalars().all() + translationservice_names = ( + run_sql(select(TranslationService.service_name)).scalars().all() + ) # The SQLAlchemy query returns a list of tuples even when values of a # single column were requested, so they must be unpacked. # TODO Add "Manual" to the TranslationService-table instead of hardcoding @@ -443,16 +450,28 @@ def add_api_key() -> Response: translator = req_data.get("translator", "") key = req_data.get("apikey", "") - tr = db.session.execute(select(TranslationService).filter( - translator == TranslationService.service_name - )).scalars().first() + tr = ( + run_sql( + select(TranslationService).filter( + translator == TranslationService.service_name + ) + ) + .scalars() + .first() + ) verify_logged_in() user = get_current_user_object() - duplicate = db.session.execute(select(TranslationServiceKey).filter( - (tr.id == TranslationServiceKey.service_id) & - (user.get_personal_group().id == TranslationServiceKey.group_id) - )).scalars().first() + duplicate = ( + run_sql( + select(TranslationServiceKey).filter( + (tr.id == TranslationServiceKey.service_id) + & (user.get_personal_group().id == TranslationServiceKey.group_id) + ) + ) + .scalars() + .first() + ) if duplicate: raise RouteException("There is already a key for this translator for this user") @@ -482,11 +501,15 @@ def remove_api_key() -> Response: translator = req_data.get("translator", "") key = req_data.get("apikey", "") - db.session.execution(delete(TranslationServiceKey).filter( - (key == TranslationServiceKey.api_key) & - (TranslationServiceKey.group_id == user.get_personal_group().id) & - (translator == TranslationService.service_name) - ).execution_options(synchronize_session=False)) + db.session.execution( + delete(TranslationServiceKey) + .filter( + (key == TranslationServiceKey.api_key) + & (TranslationServiceKey.group_id == user.get_personal_group().id) + & (translator == TranslationService.service_name) + ) + .execution_options(synchronize_session=False) + ) db.session.commit() @@ -510,9 +533,15 @@ def get_quota(): # Get the translation service by the provided service name. # TODO Maybe change to use id instead? - tr = db.session.execute(select(TranslationService).filter( - translator == TranslationService.service_name - )).scalars().first() + tr = ( + run_sql( + select(TranslationService).filter( + translator == TranslationService.service_name + ) + ) + .scalars() + .first() + ) tr.register(get_current_user_object().get_personal_group()) return json_response(tr.usage()) @@ -533,9 +562,15 @@ def get_valid_status() -> Response: key = req_data.get("apikey", "") # Get the translation service by the provided service name. - tr = db.session.execute(select(TranslationService).filter( - translator == TranslationService.service_name, - )).scalars().first() + tr = ( + run_sql( + select(TranslationService).filter( + translator == TranslationService.service_name, + ) + ) + .scalars() + .first() + ) # Each new translator engine should add their preferred method for # validating api keys here. @@ -565,9 +600,15 @@ def get_keys() -> Response: verify_logged_in() user = get_current_user_object() - keys = db.session.execute(select(TranslationServiceKey).filter( - TranslationServiceKey.group_id == user.get_personal_group().id - )).scalars().all() + keys = ( + run_sql( + select(TranslationServiceKey).filter( + TranslationServiceKey.group_id == user.get_personal_group().id + ) + ) + .scalars() + .all() + ) return json_response(keys) @@ -583,9 +624,15 @@ def get_my_translators() -> Response: verify_logged_in() user = get_current_user_object() - keys = db.session.execute(select(TranslationServiceKey).filter( - TranslationServiceKey.group_id == user.get_personal_group().id - )).scalars().all() + keys = ( + run_sql( + select(TranslationServiceKey).filter( + TranslationServiceKey.group_id == user.get_personal_group().id + ) + ) + .scalars() + .all() + ) result = [] for x in keys: diff --git a/timApp/document/translation/translator.py b/timApp/document/translation/translator.py index 30d0f25a5e..b24c02a549 100644 --- a/timApp/document/translation/translator.py +++ b/timApp/document/translation/translator.py @@ -33,7 +33,7 @@ Table, Translate, ) -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup from timApp.util import logger @@ -202,7 +202,7 @@ def get_by_user_group( :return: The first matching TranslationServiceKey instance, if one is found. """ - return db.session.execute( + return run_sql( select(TranslationServiceKey).filter( TranslationServiceKey.group_id == user_group ) @@ -275,7 +275,7 @@ def __init__( """ translator = ( - db.session.execute( + run_sql( select(with_polymorphic(TranslationService, "*")).filter( TranslationService.service_name == translator_code ) diff --git a/timApp/folder/folder.py b/timApp/folder/folder.py index 290bfe43ad..0187a9a370 100644 --- a/timApp/folder/folder.py +++ b/timApp/folder/folder.py @@ -13,7 +13,7 @@ from timApp.item.block import Block, insert_block, copy_default_rights, BlockType from timApp.item.item import Item from timApp.timdb.exceptions import ItemAlreadyExistsException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup from timApp.util.utils import split_location, join_location, relative_location @@ -53,7 +53,7 @@ def get_by_id(fid) -> Folder | None: @staticmethod def find_by_location(location, name) -> Folder | None: return ( - db.session.execute(select(Folder).filter_by(name=name, location=location)) + run_sql(select(Folder).filter_by(name=name, location=location)) .scalars() .first() ) @@ -120,7 +120,7 @@ def get_all_in_path( stmt = select(Folder).filter(f_filter) if filter_ids: stmt = stmt.filter(Folder.id.in_(filter_ids)) - return db.session.execute(stmt).scalars().all() + return run_sql(stmt).scalars().all() def is_root(self) -> bool: return self.id == -1 @@ -128,8 +128,8 @@ def is_root(self) -> bool: def delete(self): assert self.is_empty db.session.delete(self) - db.session.execute(delete(BlockAccess).where(BlockAccess.block_id == self.id)) - db.session.execute( + run_sql(delete(BlockAccess).where(BlockAccess.block_id == self.id)) + run_sql( delete(Block).where( (Block.type_id == BlockType.Folder.value) & (Block.id == self.id) ) @@ -155,9 +155,7 @@ def rename_path(self, new_path: str) -> None: def rename_content(self, old_path: str, new_path: str): """Renames contents of the folder.""" docs_in_folder: list[DocEntry] = ( - db.session.execute( - select(DocEntry).filter(DocEntry.name.like(old_path + "/%")) - ) + run_sql(select(DocEntry).filter(DocEntry.name.like(old_path + "/%"))) .scalars() .all() ) @@ -165,7 +163,7 @@ def rename_content(self, old_path: str, new_path: str): d.name = d.name.replace(old_path, new_path, 1) folders_in_folder = ( - db.session.execute( + run_sql( select(Folder).filter( (Folder.location == old_path) | (Folder.location.like(old_path + "/%")) @@ -180,10 +178,10 @@ def rename_content(self, old_path: str, new_path: str): @property def is_empty(self): stmt = select(Folder.id).filter_by(location=self.path) - if db.session.execute(stmt.limit()).first(): + if run_sql(stmt.limit()).first(): return False stmt = select(DocEntry.id).filter(DocEntry.name.like(self.path + "/%")) - return not db.session.execute(stmt.limit(1)).first() + return not run_sql(stmt.limit(1)).first() @property def parent(self) -> Folder | None: @@ -224,7 +222,7 @@ def get_document( self, relative_path: str, create_if_not_exist=False, creator_group=None ) -> None | DocEntry: doc = ( - db.session.execute( + run_sql( select(DocEntry).filter_by( name=join_location(self.get_full_path(), relative_path) ) @@ -311,9 +309,7 @@ def create( rel_path, rel_name = split_location(path) folder = ( - db.session.execute( - select(Folder).filter_by(name=rel_name, location=rel_path) - ) + run_sql(select(Folder).filter_by(name=rel_name, location=rel_path)) .scalars() .first() ) diff --git a/timApp/gamification/gamificationdata.py b/timApp/gamification/gamificationdata.py index afbbf48f1b..ef48b7e069 100644 --- a/timApp/gamification/gamificationdata.py +++ b/timApp/gamification/gamificationdata.py @@ -10,7 +10,7 @@ from timApp.document.viewcontext import default_view_ctx from timApp.document.yamlblock import YamlBlock from timApp.plugin.plugin import find_task_ids -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql def gamify(initial_data: YamlBlock): @@ -87,7 +87,9 @@ def get_sorted_lists(items, item_name: str): filtered_items.append(item) # Sort both so they can be looped simultaneusly without value mismatches. docs = sorted( - db.session.execute(select(DocEntry).filter(DocEntry.name.in_(item_path_list))).scalars().all(), + run_sql(select(DocEntry).filter(DocEntry.name.in_(item_path_list))) + .scalars() + .all(), key=attrgetter("path"), ) items = sorted(filtered_items, key=itemgetter("path")) diff --git a/timApp/item/distribute_rights.py b/timApp/item/distribute_rights.py index 54c0268e00..dcaf100db6 100644 --- a/timApp/item/distribute_rights.py +++ b/timApp/item/distribute_rights.py @@ -22,7 +22,7 @@ from timApp.folder.folder import Folder from timApp.item.item import Item from timApp.tim_app import app, csrf -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.user.userutils import grant_access @@ -259,7 +259,7 @@ def get_group_emails(self, r: GroupOp) -> list[Email]: if not emails: emails = [ e - for e in db.session.execute( + for e in run_sql( select(User.email) .join(User, UserGroup.users) .filter(UserGroup.name == r.group) @@ -438,7 +438,7 @@ def receive_right( secret: str, ) -> Response: check_secret(secret, "DIST_RIGHTS_RECEIVE_SECRET") - uges = db.session.execute( + uges = run_sql( select(UserGroup, User.email) .join(User, UserGroup.name == User.name) .filter(User.email.in_(re.email for re in rights)) @@ -543,7 +543,7 @@ def get_current_rights_route( except FileNotFoundError: raise RouteException(f"Unknown target: {target}") groups_list = groups.split(",") - emails = db.session.execute( + emails = run_sql( select(User.email) .join(UserGroup, User.groups) .filter(UserGroup.name.in_(groups_list)) diff --git a/timApp/item/item.py b/timApp/item/item.py index c93149d03c..1f9793111f 100644 --- a/timApp/item/item.py +++ b/timApp/item/item.py @@ -12,7 +12,7 @@ from timApp.item.block import Block, BlockType from timApp.item.blockrelevance import BlockRelevance from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import include_if_loaded, db +from timApp.timdb.sqa import include_if_loaded, db, run_sql from timApp.util.utils import split_location, date_to_relative, cached_property if TYPE_CHECKING: @@ -133,7 +133,7 @@ def parents_to_root(self, include_root=True, eager_load_groups=False): .selectinload(Block.accesses) .joinedload(BlockAccess.usergroup) ) - crumbs = db.session.execute(crumbs_stmt).scalars().all() + crumbs = run_sql(crumbs_stmt).scalars().all() if include_root: crumbs.append(Folder.get_root()) return crumbs diff --git a/timApp/item/manage.py b/timApp/item/manage.py index 4294660747..a4544b8804 100644 --- a/timApp/item/manage.py +++ b/timApp/item/manage.py @@ -53,7 +53,7 @@ FieldSaveRequest, FieldSaveUserEntry, ) -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User, ItemOrBlock from timApp.user.usergroup import UserGroup from timApp.user.users import ( @@ -129,7 +129,7 @@ def manage(path: str) -> Response | str: js=["angular-ui-grid"], jsMods=get_grid_modules(), orgs=UserGroup.get_organizations(), - access_types=db.session.execute(select(AccessTypeModel)).scalars().all(), + access_types=run_sql(select(AccessTypeModel)).scalars().all(), ) @@ -211,9 +211,7 @@ def __post_init__(self): @cached_property def group_objects(self): return ( - db.session.execute( - select(UserGroup).filter(UserGroup.name.in_(self.groups)) - ) + run_sql(select(UserGroup).filter(UserGroup.name.in_(self.groups))) .scalars() .all() ) @@ -428,7 +426,7 @@ def expire_doc_velp_groups_perms(doc_id: int, ug: UserGroup) -> None: if is_velp_group_in_document(vg, doc): # TODO Should this apply to ALL permissions, instead of just 'view'? acc: BlockAccess | None = ( - db.session.execute( + run_sql( select(BlockAccess).filter_by( type=AccessType.view.value, block_id=vg.id, @@ -459,7 +457,7 @@ def expire_doc_translation_perms(doc_id: int, ug: UserGroup) -> None: continue # TODO Should this apply to ALL permissions, instead of just 'view'? acc: BlockAccess | None = ( - db.session.execute( + run_sql( select(BlockAccess).filter_by( type=AccessType.view.value, block_id=tr.id, @@ -498,7 +496,7 @@ def do_confirm_permission( confirm_translations: bool = True, ): ba: BlockAccess | None = ( - db.session.execute( + run_sql( select(BlockAccess).filter_by( type=m.type.value, block_id=m.id, @@ -547,7 +545,7 @@ def edit_permissions(m: PermissionMassEditModel) -> Response: if nonexistent: raise RouteException(f"Non-existent groups: {nonexistent}") items: list[ItemOrBlock] = ( - db.session.execute( + run_sql( select(Block) .filter( Block.id.in_(m.ids) @@ -922,7 +920,7 @@ def get_permissions(item_id: int) -> Response: return json_response( { "grouprights": grouprights, - "accesstypes": db.session.execute(select(AccessTypeModel)).scalars().all(), + "accesstypes": run_sql(select(AccessTypeModel)).scalars().all(), "orgs": UserGroup.get_organizations(), }, date_conversion=True, diff --git a/timApp/item/routes.py b/timApp/item/routes.py index 93477ff898..0c09a3c4e7 100644 --- a/timApp/item/routes.py +++ b/timApp/item/routes.py @@ -109,7 +109,7 @@ from timApp.readmark.readings import mark_all_read from timApp.tim_app import app from timApp.timdb.exceptions import PreambleException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.groups import verify_group_view_access from timApp.user.settings.style_utils import resolve_themes from timApp.user.settings.styles import generate_style @@ -378,14 +378,11 @@ def gen_cache( # Compute users from the current rights. accesses: ValuesView[BlockAccess] = doc_info.block.accesses.values() group_ids = {a.usergroup_id for a in accesses if not a.expired} - users: list[tuple[User, UserGroup]] = ( - db.session.execute( - select(User, UserGroup) - .join(UserGroup, User.groups) - .filter(UserGroup.id.in_(group_ids)) - ) - .all() - ) + users: list[tuple[User, UserGroup]] = run_sql( + select(User, UserGroup) + .join(UserGroup, User.groups) + .filter(UserGroup.id.in_(group_ids)) + ).all() groups_that_need_access_check = { g for u, g in users if u.get_personal_group() != g } @@ -774,9 +771,7 @@ def render_doc_view( ugs_without_access = [] if usergroups is not None: ugs = ( - db.session.execute( - select(UserGroup).filter(UserGroup.name.in_(usergroups)) - ) + run_sql(select(UserGroup).filter(UserGroup.name.in_(usergroups))) .scalars() .all() ) @@ -1183,7 +1178,7 @@ def get_linked_groups(i: Item) -> tuple[list[UserGroupWithSisuInfo], list[str]]: list( map( UserGroupWithSisuInfo, - db.session.execute( + run_sql( get_usergroup_eager_query().filter( UserGroup.name.in_(group_tags) ) diff --git a/timApp/item/routes_tags.py b/timApp/item/routes_tags.py index e5df31847e..2e929c1083 100644 --- a/timApp/item/routes_tags.py +++ b/timApp/item/routes_tags.py @@ -20,7 +20,7 @@ from timApp.document.docinfo import DocInfo from timApp.item.block import Block from timApp.item.tag import Tag, TagType, GROUP_TAG_PREFIX -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.groups import verify_group_view_access from timApp.user.special_group_names import TEACHERS_GROUPNAME from timApp.user.usergroup import UserGroup @@ -152,7 +152,7 @@ def edit_tag(doc: str, old_tag: TagInfo, new_tag: TagInfo) -> Response: new_tag_obj = Tag(name=new_tag_name, expires=new_tag.expires, type=new_tag.type) old_tag_obj = ( - db.session.execute( + run_sql( select(Tag).filter_by(block_id=d.id, name=old_tag.name, type=old_tag.type) ) .scalars() @@ -187,9 +187,7 @@ def remove_tag(doc: str, tag: TagInfo) -> Response: verify_manage_access(d) tag_obj = ( - db.session.execute( - select(Tag).filter_by(block_id=d.id, name=tag.name, type=tag.type) - ) + run_sql(select(Tag).filter_by(block_id=d.id, name=tag.name, type=tag.type)) .scalars() .first() ) @@ -228,7 +226,7 @@ def get_all_tags() -> Response: of expiration. :returns The list of all unique tag names as list of strings. """ - tags = db.session.execute(select(Tag)).scalars().all() + tags = run_sql(select(Tag)).scalars().all() tags_unique = set() for tag in tags: diff --git a/timApp/item/taskblock.py b/timApp/item/taskblock.py index 87c586c685..bd02c7657f 100644 --- a/timApp/item/taskblock.py +++ b/timApp/item/taskblock.py @@ -4,7 +4,7 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.item.block import Block, BlockType, insert_block -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup @@ -17,18 +17,12 @@ class TaskBlock(DbModel): @staticmethod def get_by_task(task_id: str) -> TaskBlock | None: - return ( - db.session.execute(select(TaskBlock).filter_by(task_id=task_id)) - .scalars() - .first() - ) + return run_sql(select(TaskBlock).filter_by(task_id=task_id)).scalars().first() @staticmethod def get_block_by_task(task_id: str) -> Block | None: task_block = ( - db.session.execute(select(TaskBlock).filter_by(task_id=task_id)) - .scalars() - .first() + run_sql(select(TaskBlock).filter_by(task_id=task_id)).scalars().first() ) if task_block is not None: return task_block.block diff --git a/timApp/lecture/askedjson.py b/timApp/lecture/askedjson.py index 4de1aeba86..efd68e8f9f 100644 --- a/timApp/lecture/askedjson.py +++ b/timApp/lecture/askedjson.py @@ -5,7 +5,7 @@ from sqlalchemy import select from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.timdb.types import DbModel if TYPE_CHECKING: @@ -33,11 +33,7 @@ def to_json(self, hide_points=False): def get_asked_json_by_hash(json_hash: str) -> AskedJson | None: - return ( - db.session.execute(select(AskedJson).filter_by(hash=json_hash)) - .scalars() - .first() - ) + return run_sql(select(AskedJson).filter_by(hash=json_hash)).scalars().first() # NOTE: Do NOT add more fields here for new qst attributes. These are ONLY for backward compatibility. diff --git a/timApp/lecture/askedquestion.py b/timApp/lecture/askedquestion.py index 1e79733aff..f295aa2a32 100644 --- a/timApp/lecture/askedquestion.py +++ b/timApp/lecture/askedquestion.py @@ -8,7 +8,7 @@ from timApp.lecture.question_utils import qst_rand_array, qst_filter_markup_points from timApp.lecture.questionactivity import QuestionActivityKind, QuestionActivity -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import datetime_tz, DbModel from timApp.util.utils import get_current_time @@ -144,7 +144,7 @@ def get_asked_question(asked_id: int) -> AskedQuestion | None: @contextmanager def user_activity_lock(user: "User"): - db.session.execute(select(func.pg_advisory_xact_lock(user.id))) + run_sql(select(func.pg_advisory_xact_lock(user.id))) yield return # db.session.query(func.pg_advisory_lock(user.id)).all() diff --git a/timApp/lecture/lecture.py b/timApp/lecture/lecture.py index c51dbc1289..f08c8b13b0 100644 --- a/timApp/lecture/lecture.py +++ b/timApp/lecture/lecture.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import mapped_column, Mapped, DynamicMapped, relationship from timApp.lecture.lectureusers import LectureUsers -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import datetime_tz, DbModel from timApp.util.utils import get_current_time @@ -56,9 +56,7 @@ def find_by_id(lecture_id: int) -> Optional["Lecture"]: @staticmethod def find_by_code(lecture_code: str, doc_id: int) -> Optional["Lecture"]: return ( - db.session.execute( - select(Lecture).filter_by(lecture_code=lecture_code, doc_id=doc_id) - ) + run_sql(select(Lecture).filter_by(lecture_code=lecture_code, doc_id=doc_id)) .scalars() .first() ) @@ -70,7 +68,7 @@ def get_all_in_document( if not time: time = datetime.min.replace(tzinfo=timezone.utc) return ( - db.session.execute( + run_sql( select(Lecture) .filter_by(doc_id=doc_id) .filter(Lecture.end_time > time) diff --git a/timApp/lecture/lectureanswer.py b/timApp/lecture/lectureanswer.py index 231fd19e38..5a3e12fd5c 100644 --- a/timApp/lecture/lectureanswer.py +++ b/timApp/lecture/lectureanswer.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import lazyload, mapped_column, Mapped, relationship from timApp.lecture.lecture import Lecture -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import datetime_tz, DbModel from timApp.user.user import User @@ -98,4 +98,4 @@ def get_totals( .order_by(User.name) .with_only_columns(User, func.sum(LectureAnswer.points), func.count()) ) - return db.session.execute(stmt).all() + return run_sql(stmt).all() diff --git a/timApp/lecture/routes.py b/timApp/lecture/routes.py index 7f83b08a85..6c0699c531 100644 --- a/timApp/lecture/routes.py +++ b/timApp/lecture/routes.py @@ -54,7 +54,7 @@ from timApp.lecture.showpoints import ShowPoints from timApp.lecture.useractivity import UserActivity from timApp.plugin.qst.qst import get_question_data_from_document -from timApp.timdb.sqa import db, tim_main_execute +from timApp.timdb.sqa import db, tim_main_execute, run_sql from timApp.user.user import User from timApp.util.error_handlers import suppress_wuff from timApp.util.flask.requesthelper import ( @@ -595,11 +595,7 @@ def get_lecture_users(lecture: Lecture): lecturers = [] students = [] - activity = ( - db.session.execute(select(UserActivity).filter_by(lecture=lecture)) - .scalars() - .all() - ) + activity = run_sql(select(UserActivity).filter_by(lecture=lecture)).scalars().all() cur_time = get_current_time() for ac in activity: @@ -722,7 +718,7 @@ def clean_dictionaries_by_lecture(lecture: Lecture): stop_showing_points(lecture) for a in lecture.useractivity: db.session.delete(a) - db.session.execute( + run_sql( delete(QuestionActivity) .where( ( @@ -755,7 +751,7 @@ def delete_question_temp_data(question: AskedQuestion, lecture: Lecture): QuestionActivityKind.Pointsshown, ], ) - db.session.execute( + run_sql( delete(RunningQuestion).where(RunningQuestion.lecture_id == lecture.lecture_id) ) stop_showing_points(lecture) @@ -784,7 +780,7 @@ def delete_lecture(m: DeleteLectureModel): with db.session.no_autoflush: empty_lecture(lecture) for t in (Message, LectureAnswer, AskedQuestion): - db.session.execute(delete(t).where(t.lecture_id == lecture.lecture_id)) + run_sql(delete(t).where(t.lecture_id == lecture.lecture_id)) db.session.delete(lecture) db.session.commit() @@ -999,7 +995,7 @@ def show_points(m: ShowAnswerPointsModel): def stop_showing_points(lecture: Lecture): - db.session.execute( + run_sql( delete(ShowPoints) .where( ShowPoints.asked_id.in_( @@ -1034,7 +1030,7 @@ def update_question_points(): def delete_activity(question: AskedQuestion, kinds): - db.session.execute( + run_sql( delete(QuestionActivity) .where( (QuestionActivity.asked_id == question.asked_id) diff --git a/timApp/messaging/messagelist/emaillist.py b/timApp/messaging/messagelist/emaillist.py index e0ebf17541..f1a58bcf2c 100644 --- a/timApp/messaging/messagelist/emaillist.py +++ b/timApp/messaging/messagelist/emaillist.py @@ -20,7 +20,7 @@ MessageListMember, ) from timApp.tim_app import app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.user import User, deleted_user_pattern from timApp.util.flask.requesthelper import NotExist, RouteException from timApp.util.logger import log_warning, log_info, log_error @@ -806,12 +806,12 @@ def update_mailing_list_address(old: str, new: str) -> None: & (MessageListModel.mailman_list_id == new_member.list_id) ) ) - db.session.execute( + run_sql( delete(MessageListExternalMember) .where(MessageListExternalMember.id.in_(delete_ids_stmt)) .execution_options(synchronize_session=False) ) - db.session.execute( + run_sql( delete(MessageListMember) .where(MessageListMember.id.in_(delete_ids_stmt)) .execution_options(synchronize_session=False) diff --git a/timApp/messaging/messagelist/messagelist_models.py b/timApp/messaging/messagelist/messagelist_models.py index 7a00c52654..facc4ca5f2 100644 --- a/timApp/messaging/messagelist/messagelist_models.py +++ b/timApp/messaging/messagelist/messagelist_models.py @@ -15,7 +15,7 @@ Distribution, MessageVerificationType, ) -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import datetime_tz, DbModel from timApp.util.utils import get_current_time @@ -121,7 +121,7 @@ class MessageListModel(DbModel): def get_by_email(email: str) -> Optional["MessageListModel"]: name, domain = email.split("@", 1) return ( - db.session.execute( + run_sql( select(MessageListModel).filter_by(name=name, email_list_domain=domain) ) .scalars() @@ -131,7 +131,7 @@ def get_by_email(email: str) -> Optional["MessageListModel"]: @staticmethod def from_manage_doc_id(doc_id: int) -> "MessageListModel": return ( - db.session.execute(select(MessageListModel).filter_by(manage_doc_id=doc_id)) + run_sql(select(MessageListModel).filter_by(manage_doc_id=doc_id)) .scalars() .one() ) @@ -158,7 +158,7 @@ def get_by_name(name_candidate: str) -> Optional["MessageListModel"]: :return: Return the message list after query by name. Returns at most one result or None if no there are hits. """ return ( - db.session.execute(select(MessageListModel).filter_by(name=name_candidate)) + run_sql(select(MessageListModel).filter_by(name=name_candidate)) .scalars() .first() ) @@ -170,7 +170,7 @@ def name_exists(name_candidate: str) -> bool: :param name_candidate: The name we are checking if it already is already in use by another list. """ return ( - db.session.execute( + run_sql( select(MessageListModel.name).filter_by(name=name_candidate).limit(1) ) .scalars() diff --git a/timApp/messaging/messagelist/messagelist_utils.py b/timApp/messaging/messagelist/messagelist_utils.py index 5ed567f9ed..7deed577d7 100644 --- a/timApp/messaging/messagelist/messagelist_utils.py +++ b/timApp/messaging/messagelist/messagelist_utils.py @@ -57,7 +57,7 @@ MessageListExternalMember, MessageListMember, ) -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.upload.upload import upload_image_or_file_impl from timApp.upload.uploadedfile import UploadedFile from timApp.user.groups import verify_groupadmin @@ -696,9 +696,7 @@ def get_message_list_owners(mlist: MessageListModel) -> list[UserGroup]: :return: A list of owners, as their personal user group. """ manage_doc_block = ( - db.session.execute(select(Block).filter_by(id=mlist.manage_doc_id)) - .scalars() - .one() + run_sql(select(Block).filter_by(id=mlist.manage_doc_id)).scalars().one() ) return manage_doc_block.owners @@ -1312,13 +1310,11 @@ def sync_usergroup_messagelist_members( .filter(User.id.in_(user_ids)) .options(load_only(User.id, User.email, User.real_name)) ) - users = {user.id: user for user in db.session.execute(user_stmt).scalars()} + users = {user.id: user for user in run_sql(user_stmt).scalars()} try: for ug_id, diff in diffs.items(): ug_memberships = ( - db.session.execute( - select(MessageListTimMember).filter_by(group_id=ug_id) - ) + run_sql(select(MessageListTimMember).filter_by(group_id=ug_id)) .scalars() .all() ) diff --git a/timApp/messaging/timMessage/internalmessage_models.py b/timApp/messaging/timMessage/internalmessage_models.py index 1d6e2ead0c..6ecfe74f76 100644 --- a/timApp/messaging/timMessage/internalmessage_models.py +++ b/timApp/messaging/timMessage/internalmessage_models.py @@ -5,7 +5,7 @@ from sqlalchemy import func, select, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: @@ -137,7 +137,7 @@ def get_for_user( user: "User", message: InternalMessage ) -> Optional["InternalMessageReadReceipt"]: return ( - db.session.execute( + run_sql( select(InternalMessageReadReceipt).filter_by(user=user, message=message) ) .scalars() diff --git a/timApp/messaging/timMessage/routes.py b/timApp/messaging/timMessage/routes.py index 00541d860e..07d9f58cf1 100644 --- a/timApp/messaging/timMessage/routes.py +++ b/timApp/messaging/timMessage/routes.py @@ -40,7 +40,7 @@ InternalMessageDisplay, InternalMessageReadReceipt, ) -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.user.usergroupmember import UserGroupMember @@ -146,7 +146,7 @@ def expire_tim_message(message_doc_id: int) -> Response: """ internal_message: InternalMessage | None = ( - db.session.execute(select(InternalMessage).filter_by(doc_id=message_doc_id)) + run_sql(select(InternalMessage).filter_by(doc_id=message_doc_id)) .scalars() .first() ) @@ -215,7 +215,7 @@ def get_tim_messages_as_list(item_id: int | None = None) -> list[TimMessageData] .filter((is_global | is_user_specific) & can_see) ) - messages: list[InternalMessage] = db.session.execute(stmt).scalars().all() + messages: list[InternalMessage] = run_sql(stmt).scalars().all() full_messages = [] for message in messages: @@ -269,9 +269,7 @@ def get_read_receipt(doc_id: int) -> Response: :return: """ message = ( - db.session.execute(select(InternalMessage).filter_by(doc_id=doc_id)) - .scalars() - .first() + run_sql(select(InternalMessage).filter_by(doc_id=doc_id)).scalars().first() ) if not message: raise NotExist("No active messages for the document found") @@ -495,9 +493,7 @@ def mark_as_read(message_id: int) -> Response: verify_logged_in() message = ( - db.session.execute(select(InternalMessage).filter_by(id=message_id)) - .scalars() - .first() + run_sql(select(InternalMessage).filter_by(id=message_id)).scalars().first() ) if not message: raise NotExist("Message not found by the ID") @@ -527,7 +523,7 @@ def cancel_read_receipt(message_id: int) -> Response: verify_logged_in() receipt = ( - db.session.execute( + run_sql( select(InternalMessageReadReceipt).filter_by( user_id=get_current_user_object().id, message_id=message_id ) @@ -565,7 +561,7 @@ def get_read_receipts( raise NotExist("No document found") verify_manage_access(doc) - read_users = db.session.execute( + read_users = run_sql( select( InternalMessageReadReceipt.user_id, InternalMessageReadReceipt.marked_as_read_on, @@ -583,7 +579,7 @@ def get_read_receipts( } all_recipients = ( - db.session.execute( + run_sql( select(User) .join(UserGroupMember, User.active_memberships) .join( @@ -603,7 +599,7 @@ def get_read_receipts( "For performance reasons, only read users can be shown for global messages" ) all_recipients = ( - db.session.execute(select(User).filter(User.id.in_(read_user_map.keys()))) + run_sql(select(User).filter(User.id.in_(read_user_map.keys()))) .scalars() .all() ) @@ -676,7 +672,7 @@ def get_recipient_users(recipients: list[str] | None) -> list[UserGroup]: & (MessageListTimMember.membership_ended == None) ) ) - ugs = db.session.execute(stmt).scalars().all() + ugs = run_sql(stmt).scalars().all() users.update(ugs) return list(users) diff --git a/timApp/note/notes.py b/timApp/note/notes.py index 00221a64cf..0b22010b09 100644 --- a/timApp/note/notes.py +++ b/timApp/note/notes.py @@ -7,7 +7,7 @@ from timApp.document.document import Document from timApp.markdown.markdownconverter import md_to_html from timApp.note.usernote import UserNote -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup @@ -67,7 +67,7 @@ def get_notes( .filter(f) .order_by(UserNote.id) ) - return process_notes(db.session.execute(stmt).all()) + return process_notes(run_sql(stmt).all()) def move_notes(src_par: DocParagraph, dest_par: DocParagraph): @@ -80,7 +80,7 @@ def move_notes(src_par: DocParagraph, dest_par: DocParagraph): ) == str(dest_par.get_id()): return - for u in db.session.execute( + for u in run_sql( select(UserNote).filter_by(doc_id=src_par.doc.doc_id, par_id=src_par.get_id()) ).scalars(): u.doc_id = dest_par.doc.doc_id diff --git a/timApp/note/routes.py b/timApp/note/routes.py index 1f79a51e73..745d1e5a45 100644 --- a/timApp/note/routes.py +++ b/timApp/note/routes.py @@ -33,7 +33,7 @@ from timApp.notification.notify import notify_doc_watchers from timApp.notification.pending_notification import PendingNotification from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.util.flask.requesthelper import RouteException, NotExist from timApp.util.flask.responsehelper import json_response @@ -143,7 +143,7 @@ def get_notes( time_restriction = time_restriction & (UserNote.created < end) d_ids = [d.id for d in docs] ns = ( - db.session.execute( + run_sql( select(UserNote) .filter(UserNote.doc_id.in_(d_ids) & access_restriction & time_restriction) .options(selectinload(UserNote.usergroup)) @@ -159,7 +159,7 @@ def get_notes( deleted_notes = list( map( DeletedNote, - db.session.execute( + run_sql( select(PendingNotification) .filter( PendingNotification.doc_id.in_(d_ids) diff --git a/timApp/notification/notify.py b/timApp/notification/notify.py index 6d01c14a58..27417c1bd6 100644 --- a/timApp/notification/notify.py +++ b/timApp/notification/notify.py @@ -31,7 +31,7 @@ from timApp.notification.send_email import send_email from timApp.tim_app import app from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.util.flask.responsehelper import json_response, ok_response from timApp.util.flask.typedblueprint import TypedBlueprint @@ -93,7 +93,7 @@ def get_current_user_notifications(limit: int | None = None): if limit is not None: stmt = stmt.limit(limit) - nots = db.session.execute(stmt).scalars().all() + nots = run_sql(stmt).scalars().all() return nots diff --git a/timApp/notification/pending_notification.py b/timApp/notification/pending_notification.py index 24697ccd88..9eb532a7eb 100644 --- a/timApp/notification/pending_notification.py +++ b/timApp/notification/pending_notification.py @@ -5,7 +5,7 @@ from timApp.document.version import Version from timApp.notification.notification import NotificationType -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: @@ -93,7 +93,7 @@ def grouping_key(self) -> GroupingKey: def get_pending_notifications() -> list[PendingNotification]: return ( - db.session.execute( + run_sql( select(PendingNotification) .filter(PendingNotification.processed == None) .order_by(PendingNotification.created.asc()) diff --git a/timApp/peerreview/util/groups.py b/timApp/peerreview/util/groups.py index 0ab0fc10d9..5a76152fc9 100644 --- a/timApp/peerreview/util/groups.py +++ b/timApp/peerreview/util/groups.py @@ -10,7 +10,7 @@ from timApp.document.docinfo import DocInfo from timApp.peerreview.peerreview import PeerReview from timApp.plugin.taskid import TaskId -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.user.usergroupmember import UserGroupMember, membership_current @@ -24,7 +24,7 @@ def generate_review_groups(doc: DocInfo, task_ids: list[TaskId]) -> None: if user_groups: user_ids = [ uid - for uid in db.session.execute( + for uid in run_sql( select(UserGroupMember.user_id) .join(UserGroup, UserGroupMember.group) .filter(membership_current & (UserGroup.name.in_(user_groups))) @@ -82,9 +82,9 @@ def generate_review_groups(doc: DocInfo, task_ids: list[TaskId]) -> None: # PeerReview rows and pairings will be the same for every task, even if target did not answer to some of tasks # If target has an answer in a task, try to add it to PeerReview table. If not, just leave it empty for t in task_ids: - answers: list[Answer] = db.session.scalars( - get_latest_answers_query(t, users, valid_only) - ).all() + answers: list[Answer] = ( + run_sql(get_latest_answers_query(t, users, valid_only)).scalars().all() + ) excluded_users: list[User] = [] filtered_answers = [] for answer in answers: diff --git a/timApp/peerreview/util/peerreview_utils.py b/timApp/peerreview/util/peerreview_utils.py index 04d5173e47..ff1fe452a4 100644 --- a/timApp/peerreview/util/peerreview_utils.py +++ b/timApp/peerreview/util/peerreview_utils.py @@ -14,7 +14,7 @@ from timApp.document.docinfo import DocInfo from timApp.peerreview.peerreview import PeerReview from timApp.plugin.taskid import TaskId -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.util.flask.requesthelper import RouteException from timApp.util.utils import get_current_time @@ -25,7 +25,7 @@ def get_reviews_where_user_is_reviewer(d: DocInfo, user: User) -> list[PeerRevie stmt = get_reviews_where_user_is_reviewer_query(d, user).options( selectinload(PeerReview.reviewable) ) - return db.session.execute(stmt).scalars().all() + return run_sql(stmt).scalars().all() def get_reviews_where_user_is_reviewer_query(d: DocInfo, user: User) -> Select: @@ -33,11 +33,7 @@ def get_reviews_where_user_is_reviewer_query(d: DocInfo, user: User) -> Select: def get_all_reviews(doc: DocInfo) -> list[PeerReview]: - return ( - db.session.execute(select(PeerReview).filter_by(block_id=doc.id)) - .scalars() - .all() - ) + return run_sql(select(PeerReview).filter_by(block_id=doc.id)).scalars().all() def get_reviews_targeting_user(d: DocInfo, user: User) -> list[PeerReview]: @@ -45,7 +41,7 @@ def get_reviews_targeting_user(d: DocInfo, user: User) -> list[PeerReview]: stmt = get_reviews_targeting_user_query(d, user).options( selectinload(PeerReview.reviewable) ) - return db.session.execute(stmt).scalars().all() + return run_sql(stmt).scalars().all() def get_reviews_targeting_user_query(d: DocInfo, user: User) -> Select: @@ -60,7 +56,7 @@ def get_reviews_related_to_user(d: DocInfo, user: User) -> list[PeerReview]: (PeerReview.reviewable_id == user.id) | (PeerReview.reviewer_id == user.id) ) ) - return db.session.execute(stmt).scalars().all() + return run_sql(stmt).scalars().all() def has_review_access( @@ -79,7 +75,7 @@ def has_review_access( stmt = stmt.filter_by(task_name=task_id.task_name) if reviewable_user is not None: stmt = stmt.filter_by(reviewable_id=reviewable_user.id) - return bool(db.session.execute(stmt.limit(1)).scalars().first()) + return bool(run_sql(stmt.limit(1)).scalars().first()) def check_review_grouping(doc: DocInfo, tasks: list[TaskId]) -> bool: @@ -90,7 +86,7 @@ def check_review_grouping(doc: DocInfo, tasks: list[TaskId]) -> bool: .filter_by(block_id=doc.id) .filter(PeerReview.task_name.in_([t.task_name for t in tasks])) ) - return bool(db.session.execute(stmt.limit(1)).scalars().first()) + return bool(run_sql(stmt.limit(1)).scalars().first()) def is_peerreview_enabled(doc: DocInfo) -> bool: @@ -109,7 +105,7 @@ def get_reviews_for_document(doc: DocInfo) -> list[PeerReview]: :param doc: Document containing reviewable answers. """ return ( - db.session.execute( + run_sql( select(PeerReview).filter_by( block_id=doc.id, ) @@ -136,7 +132,7 @@ def change_peerreviewers_for_user( # TODO: Clean up; don't do one query per loop: instead fetch all users at once! for i in range(0, len(new_reviewers)): updated_user = ( - db.session.execute( + run_sql( select(PeerReview) .filter_by( block_id=doc.id, diff --git a/timApp/plugin/calendar/calendar.py b/timApp/plugin/calendar/calendar.py index 410da2e47a..5b62e89683 100644 --- a/timApp/plugin/calendar/calendar.py +++ b/timApp/plugin/calendar/calendar.py @@ -48,7 +48,7 @@ EnrollmentRight, ) from timApp.plugin.calendar.models import ExportedCalendar -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.groups import verify_group_access from timApp.user.special_group_names import LOGGED_IN_GROUPNAME from timApp.user.user import User, edit_access_set, manage_access_set @@ -365,9 +365,7 @@ def export_ical(user: User) -> Response: :return: """ user_data: ExportedCalendar = ( - db.session.execute( - select(ExportedCalendar).filter(ExportedCalendar.user_id == user.id) - ) + run_sql(select(ExportedCalendar).filter(ExportedCalendar.user_id == user.id)) .scalars() .one_or_none() ) @@ -414,7 +412,7 @@ def get_ical(opts: ICalFilterOptions) -> Response: :return: ICS file that can be exported otherwise 404 if user data does not exist. """ user_data: ExportedCalendar = ( - db.session.execute(select(ExportedCalendar).filter_by(calendar_hash=opts.key)) + run_sql(select(ExportedCalendar).filter_by(calendar_hash=opts.key)) .scalars() .one_or_none() ) @@ -568,7 +566,7 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev stmt = stmt.with_only_columns(Event) stmt = stmt.filter(timing_filter) - return db.session.execute(stmt).scalars().all() + return run_sql(stmt).scalars().all() @calendar_plugin.get("/events", model=FilterOptions) @@ -706,7 +704,7 @@ def save_events( # noinspection PyUnresolvedReferences event_ugs = ( - db.session.execute(select(UserGroup).filter(UserGroup.name.in_(event_ug_names))) + run_sql(select(UserGroup).filter(UserGroup.name.in_(event_ug_names))) .scalars() .all() ) @@ -814,7 +812,7 @@ def update_event(cal_event: CalendarEvent, event: Event) -> Event: if not modify_existing: raise AccessDenied("Cannot modify existing events via this route") event: Event = ( - db.session.execute(select(Event).filter_by(event_id=calendar_event.id)) + run_sql(select(Event).filter_by(event_id=calendar_event.id)) .scalars() .first() ) @@ -969,7 +967,7 @@ def send_email_to_enrolled_users( user_accounts = [] for user_group in enrolled_users: user_account = ( - db.session.execute(select(User).filter(User.name == user_group.name)) + run_sql(select(User).filter(User.name == user_group.name)) .scalars() .one_or_none() ) @@ -1018,7 +1016,7 @@ def update_book_message(event_id: int, booker_msg: str, booker_group: str) -> Re if not enrollment: raise NotExist() - db.session.execute( + run_sql( update(Enrollment) .where(Enrollment.event_id == enrollment.event_id) .values( diff --git a/timApp/plugin/calendar/models.py b/timApp/plugin/calendar/models.py index 996394d6d9..583209c830 100644 --- a/timApp/plugin/calendar/models.py +++ b/timApp/plugin/calendar/models.py @@ -18,7 +18,7 @@ from sqlalchemy import func, select, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import datetime_tz, DbModel from timApp.user.user import User from timApp.user.usergroup import UserGroup @@ -87,7 +87,7 @@ def get_by_event_and_user( ) -> Optional["Enrollment"]: """Returns a specific enrollment (or none) that match the user group id and event id""" return ( - db.session.execute( + run_sql( select(Enrollment).filter( Enrollment.event_id == event_id, Enrollment.usergroup_id == user_group_id, @@ -136,9 +136,7 @@ def get_or_create(tags: Iterable[str]) -> list["EventTag"]: result = [] # noinspection PyUnresolvedReferences existing_tags = ( - db.session.execute(select(EventTag).filter(EventTag.tag.in_(tags))) - .scalars() - .all() + run_sql(select(EventTag).filter(EventTag.tag.in_(tags))).scalars().all() ) existing_tags_dict = {tag.tag: tag for tag in existing_tags} for tag in tags: @@ -248,7 +246,7 @@ def enrollments_count(self) -> EnrollmentCounts: """Returns the number of enrollments in the event""" # noinspection PyUnresolvedReferences has_extras = ( - db.session.execute( + run_sql( select(EventGroup.extra) .filter( (EventGroup.event_id == self.event_id) & EventGroup.extra.is_(True) @@ -269,7 +267,7 @@ def enrollments_count(self) -> EnrollmentCounts: func.count(Enrollment.event_id).label("enrollments_count"), ) ) - res = {is_extra: count for is_extra, count in db.session.execute(stmt)} + res = {is_extra: count for is_extra, count in run_sql(stmt)} return EnrollmentCounts( res.get(False, 0), res.get(True, 0 if has_extras else None) ) @@ -286,7 +284,7 @@ def get_enrollment_right(self, user: User) -> EnrollmentRight: ug_ids = [ug.id for ug in user.groups] # noinspection PyUnresolvedReferences event_groups = ( - db.session.execute( + run_sql( select(EventGroup).filter( (EventGroup.event_id == self.event_id) & EventGroup.usergroup_id.in_(ug_ids) @@ -317,9 +315,7 @@ def get_enrollment_right(self, user: User) -> EnrollmentRight: @staticmethod def get_by_id(event_id: int) -> Optional["Event"]: return ( - db.session.execute(select(Event).filter_by(event_id=event_id)) - .scalars() - .one_or_none() + run_sql(select(Event).filter_by(event_id=event_id)).scalars().one_or_none() ) def to_json( @@ -356,7 +352,7 @@ def to_json( user_group_ids = [ug.id for ug in for_user.groups] # noinspection PyUnresolvedReferences e = ( - db.session.execute( + run_sql( select(EventGroup.extra).filter( (EventGroup.event_id == self.event_id) & (EventGroup.usergroup_id.in_(user_group_ids)) diff --git a/timApp/plugin/group_join/group_join.py b/timApp/plugin/group_join/group_join.py index 0bf956c612..08023b127c 100644 --- a/timApp/plugin/group_join/group_join.py +++ b/timApp/plugin/group_join/group_join.py @@ -10,7 +10,7 @@ from timApp.bookmark.course import update_user_course_bookmarks from timApp.document.docentry import DocEntry from timApp.document.docsettings import GroupSelfJoinSettings -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.groups import verify_group_edit_access from timApp.user.user import User from timApp.user.usergroup import UserGroup @@ -138,16 +138,14 @@ def _do_group_op( ) -> tuple[bool, bool, dict[str, str]]: user_groups: set[str] = set( g - for g, in db.session.execute( + for g, in run_sql( user.get_groups(include_expired=False).with_only_columns(UserGroup.name) ) ) result = dict.fromkeys(groups, "") ugs: list[UserGroup] = ( - db.session.execute(select(UserGroup).filter(UserGroup.name.in_(groups))) - .scalars() - .all() + run_sql(select(UserGroup).filter(UserGroup.name.in_(groups))).scalars().all() ) all_ok = True diff --git a/timApp/plugin/importdata/importData.py b/timApp/plugin/importdata/importData.py index 9c73fdf41a..94f51e3e6a 100644 --- a/timApp/plugin/importdata/importData.py +++ b/timApp/plugin/importdata/importData.py @@ -14,7 +14,7 @@ from timApp.plugin.jsrunner.jsrunner import jsrunner_run, JsRunnerParams, JsRunnerError from timApp.tim_app import csrf -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.hakaorganization import HakaOrganization from timApp.user.personaluniquecode import PersonalUniqueCode, SchacPersonalUniqueCode from timApp.user.user import User, UserInfo @@ -375,19 +375,19 @@ def answer(args: ImportDataAnswerModel) -> PluginAnswerResp: .filter_by(name=org) .with_only_columns(PersonalUniqueCode.code, User) ) - users = {c: u for c, u in db.session.execute(stmt)} + users = {c: u for c, u in run_sql(stmt)} elif id_prop == "username": stmt = select(User).filter(User.name.in_(idents)) - users = {u.name: u for u in db.session.execute(stmt).scalars()} + users = {u.name: u for u in run_sql(stmt).scalars()} elif id_prop == "id": try: stmt = select(User).filter(User.id.in_([int(i) for i in idents])) except ValueError as e: return args.make_answer_error(f"User ids must be ints ({e})") - users = {str(u.id): u for u in db.session.execute(stmt).scalars()} + users = {str(u.id): u for u in run_sql(stmt).scalars()} elif id_prop == "email": stmt = select(User).filter(User.email.in_(idents)) - users = {u.email: u for u in db.session.execute(stmt).scalars()} + users = {u.email: u for u in run_sql(stmt).scalars()} else: return args.make_answer_error( f"Invalid joinProperty: {args.markup.joinProperty}" diff --git a/timApp/plugin/jsrunner/util.py b/timApp/plugin/jsrunner/util.py index 7e3c98fae1..1ffe2be3b9 100644 --- a/timApp/plugin/jsrunner/util.py +++ b/timApp/plugin/jsrunner/util.py @@ -35,7 +35,7 @@ from timApp.plugin.plugintype import PluginType from timApp.plugin.taskid import TaskId, TaskIdAccess from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.groups import do_create_group, verify_group_edit_access from timApp.user.user import User, UserInfo, UserOrigin from timApp.user.usergroup import UserGroup @@ -79,9 +79,7 @@ def handle_jsrunner_groups(groupdata: JsrunnerGroups | None, curr_user: User) -> before=current_state, after=set(current_state) ) users: list[User] = ( - db.session.execute(select(User).filter(User.id.in_(uids))) - .scalars() - .all() + run_sql(select(User).filter(User.id.in_(uids))).scalars().all() ) found_user_ids = {u.id for u in users} missing_ids = set(uids) - found_user_ids @@ -228,7 +226,7 @@ def save_fields( doc_map: dict[int, DocInfo] = {} user_map: dict[int, User] = { u.id: u - for u in db.session.execute( + for u in run_sql( select(User).filter(User.id.in_(x["user"] for x in save_obj)) ).scalars() } @@ -355,7 +353,7 @@ def save_fields( .with_only_columns(func.max(Answer.id).label("aid"), User.id.label("uid")) .subquery() ) - datas: list[tuple[int, Answer]] = db.session.execute( + datas: list[tuple[int, Answer]] = run_sql( select(Answer) .join(sq, Answer.id == sq.c.aid) .with_only_columns(sq.c.uid, Answer) diff --git a/timApp/plugin/plugin.py b/timApp/plugin/plugin.py index 51c790bf87..e7c60d29cd 100644 --- a/timApp/plugin/plugin.py +++ b/timApp/plugin/plugin.py @@ -33,7 +33,7 @@ from timApp.plugin.taskid import TaskId, UnvalidatedTaskId, TaskIdAccess from timApp.printing.printsettings import PrintFormat from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.user import User from timApp.util.rndutils import myhash, SeedClass from timApp.util.utils import try_load_json, get_current_time, Range @@ -603,7 +603,7 @@ def set_access_end_for_user(self, user: User | None = None): if not b: return ba = ( - db.session.execute( + run_sql( select(BlockAccess).filter_by( block_id=b.id, type=AccessType.view.value, @@ -640,7 +640,7 @@ def hidden_by_prerequisite(self) -> bool: b = TaskBlock.get_by_task(tid.doc_task) if b: ba = ( - db.session.execute( + run_sql( select(BlockAccess).filter_by( block_id=b.id, type=AccessType.view.value, diff --git a/timApp/plugin/pluginControl.py b/timApp/plugin/pluginControl.py index 69e4252074..351657cb2e 100644 --- a/timApp/plugin/pluginControl.py +++ b/timApp/plugin/pluginControl.py @@ -43,7 +43,7 @@ from timApp.plugin.pluginexception import PluginException from timApp.plugin.taskid import TaskId from timApp.printing.printsettings import PrintFormat -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.user import User from timApp.util.get_fields import ( get_fields_and_users, @@ -397,7 +397,7 @@ def get_answers(user: User, task_ids, answer_map): .group_by(Answer.task_id) .subquery() ) - answers: list[tuple[Answer, int]] = db.session.execute( + answers: list[tuple[Answer, int]] = run_sql( select(Answer) .join(sub, Answer.id == sub.c.col) .with_only_columns(Answer, sub.c.cnt) diff --git a/timApp/plugin/plugintype.py b/timApp/plugin/plugintype.py index 12e6d89765..f05fa53ea4 100644 --- a/timApp/plugin/plugintype.py +++ b/timApp/plugin/plugintype.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import mapped_column, Mapped, Session import timApp -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import DbModel CONTENT_FIELD_NAME_MAP = { @@ -42,11 +42,7 @@ class PluginType(DbModel, PluginTypeBase): @staticmethod def resolve(p_type: str) -> "PluginType": - pt = ( - db.session.execute(select(PluginType).filter_by(type=p_type)) - .scalars() - .first() - ) + pt = run_sql(select(PluginType).filter_by(type=p_type)).scalars().first() if pt: return pt @@ -66,11 +62,7 @@ def resolve(p_type: str) -> "PluginType": raise # We have to re-query the database since the other session was closed - return ( - db.session.execute(select(PluginType).filter_by(type=p_type)) - .scalars() - .one() - ) + return run_sql(select(PluginType).filter_by(type=p_type)).scalars().one() def get_type(self) -> str: return self.type diff --git a/timApp/plugin/tableform/tableForm.py b/timApp/plugin/tableform/tableForm.py index 4f19b07e3b..6edd8f6c15 100644 --- a/timApp/plugin/tableform/tableForm.py +++ b/timApp/plugin/tableform/tableForm.py @@ -31,7 +31,7 @@ from timApp.sisu.parse_display_name import parse_sisu_group_display_name from timApp.sisu.sisu import get_potential_groups from timApp.tim_app import csrf -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.user import User, get_membership_end, get_membership_added from timApp.user.usergroup import UserGroup from timApp.util.flask.requesthelper import ( @@ -181,7 +181,7 @@ def get_sisugroups(user: User, sisu_id: str | None) -> "TableFormObj": gs = get_potential_groups(user, sisu_id) docs_with_course_tag = select(Tag.block_id).filter_by(type=TagType.CourseCode) tags = ( - db.session.execute( + run_sql( select(Tag) .filter( Tag.name.in_([GROUP_TAG_PREFIX + g.name for g in gs]) diff --git a/timApp/plugin/userselect/action_queue.py b/timApp/plugin/userselect/action_queue.py index a82e273ca2..1a72092747 100644 --- a/timApp/plugin/userselect/action_queue.py +++ b/timApp/plugin/userselect/action_queue.py @@ -20,7 +20,7 @@ from timApp.plugin.userselect.utils import group_expired_offset from timApp.tim_app import app from timApp.tim_celery import apply_pending_userselect_actions -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.user.usergroupmember import membership_current, UserGroupMember @@ -293,20 +293,18 @@ def apply_pending_actions_impl() -> None: # noinspection PyUnresolvedReferences group_cache: dict[str, UserGroup] = { ug.name: ug - for ug in db.session.execute( + for ug in run_sql( select(UserGroup).filter(UserGroup.name.in_(group_names)) ).scalars() } # noinspection PyUnresolvedReferences user_cache: dict[str, User] = { u.name: u - for u in db.session.execute( - select(User).filter(User.name.in_(user_names)) - ).scalars() + for u in run_sql(select(User).filter(User.name.in_(user_names))).scalars() } memberships_cache: dict[tuple[int, int], UserGroupMember] = { (m.usergroup_id, m.user_id): m - for m in db.session.execute( + for m in run_sql( select(UserGroupMember) .join(UserGroup, UserGroupMember.group) .join(User, UserGroupMember.user) diff --git a/timApp/plugin/userselect/userselect.py b/timApp/plugin/userselect/userselect.py index bdb80b4a13..b0c2666f7f 100644 --- a/timApp/plugin/userselect/userselect.py +++ b/timApp/plugin/userselect/userselect.py @@ -42,7 +42,7 @@ apply_dist_right_actions, ) from timApp.plugin.userselect.utils import group_expired_offset -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.groups import verify_group_edit_access from timApp.user.user import User from timApp.user.usergroup import UserGroup @@ -518,19 +518,13 @@ def get_groups( cur_user: User, add: list[str], remove: list[str], change_all_groups: list[str] ) -> tuple[list[UserGroup], list[UserGroup], list[UserGroup]]: add_groups: list[UserGroup] = ( - db.session.execute(select(UserGroup).filter(UserGroup.name.in_(add))) - .scalars() - .all() + run_sql(select(UserGroup).filter(UserGroup.name.in_(add))).scalars().all() ) remove_groups: list[UserGroup] = ( - db.session.execute(select(UserGroup).filter(UserGroup.name.in_(remove))) - .scalars() - .all() + run_sql(select(UserGroup).filter(UserGroup.name.in_(remove))).scalars().all() ) change_all_groups_ugs: list[UserGroup] = ( - db.session.execute( - select(UserGroup).filter(UserGroup.name.in_(change_all_groups)) - ) + run_sql(select(UserGroup).filter(UserGroup.name.in_(change_all_groups))) .scalars() .all() ) @@ -730,7 +724,7 @@ def apply_permission_actions( for to_confirm in confirm: doc_entry = doc_entries[to_confirm.doc_path] ba_confirm: BlockAccess | None = ( - db.session.execute( + run_sql( select(BlockAccess) .filter_by( type=to_confirm.type.value, @@ -751,7 +745,7 @@ def apply_permission_actions( for to_change in change_time: doc_entry = doc_entries[to_change.doc_path] ba_change: BlockAccess | None = ( - db.session.execute( + run_sql( select(BlockAccess) .filter_by( type=to_change.type.value, diff --git a/timApp/printing/documentprinter.py b/timApp/printing/documentprinter.py index c8f57ed7ed..d18bc44d6b 100644 --- a/timApp/printing/documentprinter.py +++ b/timApp/printing/documentprinter.py @@ -50,7 +50,7 @@ from timApp.printing.printeddoc import PrintedDoc from timApp.printing.printsettings import PrintFormat from timApp.timdb.dbaccess import get_files_path -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.user import User from timApp.util.utils import cache_folder_path from tim_common.html_sanitize import sanitize_html @@ -865,7 +865,7 @@ def get_printed_document_path_from_db( url_macros: dict[str, str] | None = None, ) -> str | None: existing_print: PrintedDoc | None = ( - db.session.execute( + run_sql( select(PrintedDoc) .filter_by( doc_id=self._doc_entry.id, diff --git a/timApp/readmark/readings.py b/timApp/readmark/readings.py index f2338748ff..d5a118e055 100644 --- a/timApp/readmark/readings.py +++ b/timApp/readmark/readings.py @@ -9,7 +9,7 @@ from timApp.document.document import Document from timApp.readmark.readparagraph import ReadParagraph from timApp.readmark.readparagraphtype import ReadParagraphType -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.util.utils import get_current_time @@ -23,9 +23,7 @@ def get_readings( usergroup_id: int, doc: Document, filter_condition=None ) -> list[ReadParagraph]: return ( - db.session.execute( - get_readings_filtered_query(usergroup_id, doc, filter_condition) - ) + run_sql(get_readings_filtered_query(usergroup_id, doc, filter_condition)) .scalars() .all() ) @@ -41,7 +39,7 @@ def has_anything_read(usergroup_ids: list[int], doc: Document) -> bool: & (ReadParagraph.type == ReadParagraphType.click_red) ) # Normal query is generally faster than an "exists" subquery even if it causes extra data to be loaded - return db.session.execute(query).scalars().first() is not None + return run_sql(query).scalars().first() is not None def get_readings_filtered_query( @@ -97,7 +95,7 @@ def mark_read( def mark_all_read(usergroup_id: int, doc: Document): existing = { (r.par_id, r.doc_id): r - for r in db.session.execute( + for r in run_sql( get_readings_query(usergroup_id, doc).filter( ReadParagraph.type == ReadParagraphType.click_red ) @@ -115,7 +113,7 @@ def remove_all_read_marks(doc: Document): # usually you'd use get_referenced_document_ids to get all document IDs # Since we're deleting read marks here, it's better to be safe and only remove marks only # for paragraphs defined directly in the document - db.session.execute( + run_sql( delete(ReadParagraph) .where( ReadParagraph.id.in_( @@ -144,7 +142,7 @@ def copy_readings(src_par: DocParagraph, dest_par: DocParagraph): src_par_stmt = select(ReadParagraph).filter_by( doc_id=src_par.doc.doc_id, par_id=src_par.get_id() ) - db.session.execute( + run_sql( delete(ReadParagraph) .where( (ReadParagraph.doc_id == dest_par.doc.doc_id) @@ -156,7 +154,7 @@ def copy_readings(src_par: DocParagraph, dest_par: DocParagraph): .execution_options(synchronize_session="fetch") ) - for p in db.session.execute(src_par_stmt).scalars(): # type: ReadParagraph + for p in run_sql(src_par_stmt).scalars(): # type: ReadParagraph db.session.add( ReadParagraph( usergroup_id=p.usergroup_id, diff --git a/timApp/readmark/routes.py b/timApp/readmark/routes.py index fa6bfe2e2e..39a9e9e8ae 100644 --- a/timApp/readmark/routes.py +++ b/timApp/readmark/routes.py @@ -26,7 +26,7 @@ from timApp.readmark.readparagraphtype import ReadParagraphType from timApp.sisu.sisu import IncorrectSettings from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.util.flask.requesthelper import ( @@ -130,7 +130,7 @@ def set_read_paragraph(doc_id, par_id, read_type=None, unread=False): for p in pars: if unread: rp = ( - db.session.execute( + run_sql( select(ReadParagraph) .filter_by( usergroup_id=group_id, @@ -276,12 +276,12 @@ def row_to_dict(row): return dict(zip(column_names, maybe_hide_name_from_row(row))) if result_format == "count": - reads = list(map(row_to_dict, db.session.execute(stmt).all())) + reads = list(map(row_to_dict, run_sql(stmt).all())) return Response(str(len(reads)), mimetype="text/plain") if result_format == "userid": - reads = list(map(row_to_dict, db.session.execute(stmt).all())) + reads = list(map(row_to_dict, run_sql(stmt).all())) result = "" for r in reads: @@ -294,8 +294,8 @@ def row_to_dict(row): def gen_rows(): yield column_names - yield from (maybe_hide_name_from_row(row) for row in db.session.execute(stmt)) + yield from (maybe_hide_name_from_row(row) for row in run_sql(stmt)) return csv_response(gen_rows(), dialect=csv_dialect) else: - return json_response(list(map(row_to_dict, db.session.execute(stmt).all()))) + return json_response(list(map(row_to_dict, run_sql(stmt).all()))) diff --git a/timApp/scheduling/scheduling_routes.py b/timApp/scheduling/scheduling_routes.py index 70d4546da1..78359eae19 100644 --- a/timApp/scheduling/scheduling_routes.py +++ b/timApp/scheduling/scheduling_routes.py @@ -23,7 +23,7 @@ from timApp.document.yamlblock import parse_yaml from timApp.item.block import Block, BlockType from timApp.plugin.plugin import Plugin -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import get_owned_objects_query from timApp.user.usergroup import UserGroup from timApp.util.flask.requesthelper import RouteException, NotExist @@ -79,14 +79,12 @@ def get_scheduled_functions(all_users: bool = False) -> Response: verify_admin() if not all_users: - stmt = stmt.filter( - BlockAccess.block_id.in_(get_owned_objects_query(u)) - ) + stmt = stmt.filter(BlockAccess.block_id.in_(get_owned_objects_query(u))) - scheduled_fns: list[PeriodicTask] = db.session.execute(stmt).scalars().all() + scheduled_fns: list[PeriodicTask] = run_sql(stmt).scalars().all() docentries = ( - db.session.execute( + run_sql( select(DocEntry).filter( DocEntry.id.in_([t.task_id.doc_id for t in scheduled_fns]) ) @@ -142,7 +140,7 @@ def add_scheduled_function( if secs < min_interval: raise RouteException(f"Minimum interval is {min_interval} seconds.") schedule: IntervalSchedule | None = ( - db.session.execute( + run_sql( select(IntervalSchedule) .filter_by(every=secs, period=IntervalSchedule.SECONDS) .limit(1) @@ -157,7 +155,7 @@ def add_scheduled_function( assert p.task_id is not None task_id_str = p.task_id.doc_task existing = ( - db.session.execute(select(PeriodicTask).filter_by(name=task_id_str).limit(1)) + run_sql(select(PeriodicTask).filter_by(name=task_id_str).limit(1)) .scalars() .first() ) @@ -187,7 +185,7 @@ def delete_scheduled_plugin_run( ) -> Response: pto: PeriodicTask = ( ( - db.session.execute( + run_sql( select(PeriodicTask) .select_from(Block) .filter_by(id=function_id, type_id=BlockType.ScheduledFunction.value) diff --git a/timApp/sisu/scim.py b/timApp/sisu/scim.py index 0d0c29526c..87a1a88dd8 100644 --- a/timApp/sisu/scim.py +++ b/timApp/sisu/scim.py @@ -23,7 +23,7 @@ from timApp.sisu.scimusergroup import ScimUserGroup, external_id_re from timApp.sisu.sisu import refresh_sisu_grouplist_doc, send_course_group_mail from timApp.tim_app import csrf -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.scimentity import get_meta from timApp.user.user import ( User, @@ -219,7 +219,7 @@ def get_groups(args: GetGroupsModel) -> Response: if not m: raise SCIMException(422, "Unsupported filter") groups = ( - db.session.execute( + run_sql( select(UserGroup) .select_from(ScimUserGroup) .filter(ScimUserGroup.external_id.startswith(scim_group_to_tim(m.group(1)))) @@ -391,7 +391,7 @@ def update_users(ug: UserGroup, args: SCIMGroupModel) -> None: added_users = set() scimuser = User.get_scimuser() existing_accounts: list[User] = ( - db.session.execute( + run_sql( select(User).filter( User.name.in_(current_usernames) | User.email.in_(emails) ) @@ -586,7 +586,7 @@ def members() -> Generator[dict, None, None]: def try_get_group_by_scim(group_id: str) -> UserGroup | None: try: ug = ( - db.session.execute( + run_sql( select(UserGroup) .select_from(ScimUserGroup) .filter_by(external_id=scim_group_to_tim(group_id)) diff --git a/timApp/sisu/sisu.py b/timApp/sisu/sisu.py index 18f2f50dd9..2de52f04e0 100644 --- a/timApp/sisu/sisu.py +++ b/timApp/sisu/sisu.py @@ -35,7 +35,7 @@ ) from timApp.sisu.scimusergroup import ScimUserGroup from timApp.tim_app import app, csrf -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.groups import ( validate_groupname, update_group_doc_settings, @@ -740,7 +740,7 @@ def get_sisu_assessments( else: usergroups = groups ugs = ( - db.session.execute(select(UserGroup).filter(UserGroup.name.in_(usergroups))) + run_sql(select(UserGroup).filter(UserGroup.name.in_(usergroups))) .scalars() .all() ) diff --git a/timApp/slide/routes.py b/timApp/slide/routes.py index db5fff0feb..cc7086a0e9 100644 --- a/timApp/slide/routes.py +++ b/timApp/slide/routes.py @@ -4,7 +4,7 @@ from timApp.auth.accesshelper import get_doc_or_abort, verify_manage_access from timApp.slide.slidestatus import SlideStatus -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.util.flask.responsehelper import json_response, ok_response from timApp.util.flask.typedblueprint import TypedBlueprint @@ -18,9 +18,7 @@ @slide_bp.get("/getslidestatus") def getslidestatus(doc_id: int): status: SlideStatus = ( - db.session.execute(select(SlideStatus).filter_by(doc_id=doc_id).limit(1)) - .scalars() - .first() + run_sql(select(SlideStatus).filter_by(doc_id=doc_id).limit(1)).scalars().first() ) st = status.status if status else None return json_response(json.loads(st)) diff --git a/timApp/tests/browser/test_questions.py b/timApp/tests/browser/test_questions.py index 75a431c475..9e68357891 100644 --- a/timApp/tests/browser/test_questions.py +++ b/timApp/tests/browser/test_questions.py @@ -14,7 +14,7 @@ find_button_by_text, find_by_attr_name, ) -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql ChoiceList = list[tuple[str, str]] ElementList = list[WebElement] @@ -389,7 +389,7 @@ def do_question_test( # check answer format is correct a = ( - db.session.execute( + run_sql( select(Answer).filter_by(task_id=f'{d.id}.{qst_par.get_attr("taskId")}') ) .scalars() diff --git a/timApp/tests/server/test_comments.py b/timApp/tests/server/test_comments.py index fb43797195..a1ae54bc55 100644 --- a/timApp/tests/server/test_comments.py +++ b/timApp/tests/server/test_comments.py @@ -11,7 +11,7 @@ from timApp.notification.send_email import sent_mails_in_testing from timApp.tests.server.test_notify import NotifyTestBase from timApp.tests.server.timroutetest import get_note_id_from_json -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User comment_selector = CSSSelector("div.notes > div.note") @@ -303,7 +303,7 @@ def test_comment_in_referenced_area(self): orig_par = d2.document.get_paragraphs()[0] r = self.post_comment(area_a_middle, public=True, text="test", orig=orig_par) note: UserNote = ( - db.session.execute(select(UserNote).order_by(UserNote.id.desc()).limit(1)) + run_sql(select(UserNote).order_by(UserNote.id.desc()).limit(1)) .scalars() .first() ) @@ -346,7 +346,7 @@ def test_comment_in_referenced_area(self): # start/end tags. r = self.post_comment(area_a_start, public=True, text="test", orig=orig_par) note: UserNote = ( - db.session.execute(select(UserNote).order_by(UserNote.id.desc()).limit(1)) + run_sql(select(UserNote).order_by(UserNote.id.desc()).limit(1)) .scalars() .first() ) diff --git a/timApp/tests/server/test_default_rights.py b/timApp/tests/server/test_default_rights.py index 14e5735d05..c7bd97b4c2 100644 --- a/timApp/tests/server/test_default_rights.py +++ b/timApp/tests/server/test_default_rights.py @@ -11,7 +11,7 @@ from timApp.item.block import BlockType from timApp.tests.server.timroutetest import TimRouteTest from timApp.tim_app import get_home_organization_group -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.usergroup import get_anonymous_group_id from timApp.user.users import get_rights_holders, get_default_rights_holders from timApp.user.userutils import grant_default_access, default_right_paths @@ -21,11 +21,7 @@ class DefaultRightTest(TimRouteTest): def test_document_default_rights(self): self.login_test1() doc = self.create_doc().document - docentry = ( - db.session.execute(select(DocEntry).filter_by(id=doc.doc_id)) - .scalars() - .one() - ) + docentry = run_sql(select(DocEntry).filter_by(id=doc.doc_id)).scalars().one() folder: Folder = docentry.parent folder_owner_id = folder.owners[0].id kg = get_home_organization_group() diff --git a/timApp/tests/server/test_duration.py b/timApp/tests/server/test_duration.py index 4f6c9533f4..66563351aa 100644 --- a/timApp/tests/server/test_duration.py +++ b/timApp/tests/server/test_duration.py @@ -6,7 +6,7 @@ from timApp.auth.auth_models import BlockAccess from timApp.document.docentry import DocEntry from timApp.tests.server.timroutetest import TimRouteTest -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.usergroup import UserGroup from timApp.user.userutils import get_access_type_id from timApp.user.userutils import grant_access @@ -44,7 +44,7 @@ def test_duration_unlock(self): ) self.get(d.url_relative) ba = ( - db.session.execute( + run_sql( select(BlockAccess).filter_by( usergroup_id=self.get_test_user_2_group_id(), block_id=doc_id, @@ -79,7 +79,7 @@ def test_duration_value_access_to_clamp(self): db.session.commit() self.get(d.url_relative, query_string={"unlock": "true"}) ba = ( - db.session.execute( + run_sql( select(BlockAccess).filter_by( usergroup_id=self.get_test_user_2_group_id(), block_id=d.id, @@ -177,7 +177,7 @@ def test_timed_duration_unlock(self): ) self.get(d.url_relative) ba = ( - db.session.execute( + run_sql( select(BlockAccess).filter_by( usergroup_id=self.get_test_user_2_group_id(), block_id=doc_id, diff --git a/timApp/tests/server/test_notify.py b/timApp/tests/server/test_notify.py index 33a2af8255..aeb4b1536f 100644 --- a/timApp/tests/server/test_notify.py +++ b/timApp/tests/server/test_notify.py @@ -10,7 +10,7 @@ ) from timApp.notification.send_email import sent_mails_in_testing from timApp.tests.server.timroutetest import TimRouteTest -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql class NotifyTestBase(TimRouteTest): @@ -219,7 +219,9 @@ def test_answer_link_email_and_null_doc_text_after_processing(self): }, sent_mails_in_testing[-1], ) - pns = db.session.execute(select(PendingNotification).filter_by(doc_id=d.id)).scalars().all() + pns = ( + run_sql(select(PendingNotification).filter_by(doc_id=d.id)).scalars().all() + ) for p in pns: if isinstance(p, DocumentNotification): self.assertIsNone(p.text) diff --git a/timApp/tests/server/test_peer_review.py b/timApp/tests/server/test_peer_review.py index 44799b7a7b..0deae850b4 100644 --- a/timApp/tests/server/test_peer_review.py +++ b/timApp/tests/server/test_peer_review.py @@ -4,7 +4,7 @@ from timApp.answer.answer import Answer from timApp.peerreview.peerreview import PeerReview from timApp.tests.server.timroutetest import TimRouteTest -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.usergroup import UserGroup @@ -44,14 +44,14 @@ def test_peer_review_generate(self): "Not enough users to form pairs (0 but at least 2 users needed)", r ) rq = select(PeerReview).filter_by(block_id=d.id) - self.assertEqual(0, len(db.session.execute(rq).scalars().all())) + self.assertEqual(0, len(run_sql(rq).scalars().all())) self.add_answer(d, "t", "x", user=self.test_user_1) db.session.commit() r = self.get(f"{url}?b={b}&size=1") self.assertIn( "Not enough users to form pairs (1 but at least 2 users needed)", r ) - self.assertEqual(0, len(db.session.execute(rq).scalars().all())) + self.assertEqual(0, len(run_sql(rq).scalars().all())) self.add_answer(d, "t", "x", user=self.test_user_2) db.session.commit() r = self.get(f"{url}?b={b}&size=1") @@ -61,7 +61,7 @@ def test_peer_review_generate(self): def check_peerreview_rows_t(): prs: list[PeerReview] = ( - db.session.execute( + run_sql( select(PeerReview) .filter_by(block_id=d.id) .order_by( @@ -82,7 +82,7 @@ def check_peerreview_rows_t(): # pairing without group works using everyone who answered the document check_peerreview_rows_t() - db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) + run_sql(delete(PeerReview).where(PeerReview.block_id == d.id)) d.document.add_setting("group", "testusers1") ug = UserGroup.create("testusers1") ug.users.append(self.test_user_1) @@ -92,7 +92,7 @@ def check_peerreview_rows_t(): self.get(f"{url}?b={b}&size=1") # pairing with group ignores group members who haven't answered the document check_peerreview_rows_t() - db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) + run_sql(delete(PeerReview).where(PeerReview.block_id == d.id)) self.add_answer(d, "t", "x", user=self.test_user_3) ug = UserGroup.create("testuser3isnothere") ug.users.append(self.test_user_1) @@ -102,21 +102,21 @@ def check_peerreview_rows_t(): self.get(f"{url}?b={b}&size=1") # pairing with group setting ignores users who answered but aren't in the group check_peerreview_rows_t() - db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) + run_sql(delete(PeerReview).where(PeerReview.block_id == d.id)) db.session.commit() dt = self.create_translation(d, lang="en-GB") tr_url = dt.get_url_for_view("review") self.get(f"{tr_url}?b={dt.document.get_paragraphs()[1].id}&size=1") # Peer-review generation works for one task in translated document check_peerreview_rows_t() - db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) + run_sql(delete(PeerReview).where(PeerReview.block_id == d.id)) db.session.commit() self.get( f"{url}?b={pars[3].id}&size=1", expect_status=400, expect_content={"error": "Requested block is inside an area"}, ) - self.assertEqual(0, len(db.session.execute(rq).all())) + self.assertEqual(0, len(run_sql(rq).all())) tu1_ans = self.add_answer(d, "ta1", "tu1", user=self.test_user_1) tu3_ans = self.add_answer(d, "ta2", "tu2", user=self.test_user_3) db.session.commit() @@ -125,11 +125,11 @@ def check_peerreview_rows_t(): expect_status=400, expect_content={"error": "Area revs not found"}, ) - self.assertEqual(0, len(db.session.execute(rq).all())) + self.assertEqual(0, len(run_sql(rq).all())) d.document.add_setting("group", "testusers1") self.get(f"{url}?area=rev") prs: list[PeerReview] = ( - db.session.execute( + run_sql( select(PeerReview) .filter_by(block_id=d.id) .order_by( @@ -157,14 +157,14 @@ def check_area_prs(): # Testuser1 and Testuser3 answered only to some tasks in the area => PR rows are still generated for every task check_area_prs() - db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) + run_sql(delete(PeerReview).where(PeerReview.block_id == d.id)) db.session.commit() self.get(f"{tr_url}?area=rev") # Peer generation works for an area in translated document check_area_prs() - db.session.execute(delete(PeerReview).where(PeerReview.block_id == d.id)) - all_answers = db.session.execute(select(Answer)).scalars().all() + run_sql(delete(PeerReview).where(PeerReview.block_id == d.id)) + all_answers = run_sql(select(Answer)).scalars().all() for a in all_answers: a.users_all = [] db.session.commit() @@ -176,10 +176,10 @@ def check_area_prs(): self.assertIn( "Not enough users to form pairs (1 but at least 2 users needed)", r ) - self.assertEqual(0, len(db.session.execute(rq).all())) + self.assertEqual(0, len(run_sql(rq).all())) d.document.add_setting("peer_review_allow_invalid", True) r = self.get(f"{url}?b={b}&size=1") self.assertNotIn( "Not enough users to form pairs (1 but at least 2 users needed)", r ) - self.assertEqual(2, len(db.session.execute(rq).all())) + self.assertEqual(2, len(run_sql(rq).all())) diff --git a/timApp/tests/server/test_personal_folder.py b/timApp/tests/server/test_personal_folder.py index f08f031cb3..abc1bbe4f3 100644 --- a/timApp/tests/server/test_personal_folder.py +++ b/timApp/tests/server/test_personal_folder.py @@ -3,7 +3,7 @@ from timApp.folder.folder import Folder from timApp.tests.server.timroutetest import TimRouteTest from timApp.tim_app import app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User, UserInfo @@ -47,16 +47,10 @@ def test_anon_personal_folder(self): """Make sure personal folders aren't created for each anonymous request.""" self.logout() self.get("/") - folders = ( - db.session.execute(select(Folder).filter_by(location="users")) - .scalars() - .all() - ) + folders = run_sql(select(Folder).filter_by(location="users")).scalars().all() self.get("/") folders_after = ( - db.session.execute(select(Folder).filter_by(location="users")) - .scalars() - .all() + run_sql(select(Folder).filter_by(location="users")).scalars().all() ) self.assertEqual(len(folders), len(folders_after)) diff --git a/timApp/tests/server/test_plugins.py b/timApp/tests/server/test_plugins.py index 987e2dd785..9005961a61 100644 --- a/timApp/tests/server/test_plugins.py +++ b/timApp/tests/server/test_plugins.py @@ -31,7 +31,7 @@ TEST_USER_2_USERNAME, ) from timApp.tests.server.timroutetest import TimRouteTest -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.special_group_names import ANONYMOUS_USERNAME from timApp.user.user import User from timApp.user.usergroup import UserGroup @@ -555,7 +555,7 @@ def test_broken_upload(self): self.do_plugin_upload(d, "test", "test.txt", f"{d.id}.testupload", "testupload") self.get(f"/uploads/{d.id}/testupload/testuser1/1/test.txt") a = ( - db.session.execute( + run_sql( select(Answer) .filter_by(task_id=f"{d.id}.testupload") .join(AnswerUpload) diff --git a/timApp/tests/server/test_self_expire.py b/timApp/tests/server/test_self_expire.py index c2ad84b871..ed1da28bea 100644 --- a/timApp/tests/server/test_self_expire.py +++ b/timApp/tests/server/test_self_expire.py @@ -4,7 +4,7 @@ from timApp.auth.accesstype import AccessType from timApp.document.docentry import DocEntry from timApp.tests.server.timroutetest import TimRouteTest -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.util.utils import get_current_time @@ -71,9 +71,7 @@ def test_self_expire_field(self): ) ans: list[Answer] = ( - db.session.execute(select(Answer).filter_by(task_id=f"{d.id}.test")) - .scalars() - .all() + run_sql(select(Answer).filter_by(task_id=f"{d.id}.test")).scalars().all() ) self.assertEqual(len(ans), 1) self.assertEqual([u.name for u in ans[0].users_all], [self.test_user_2.name]) diff --git a/timApp/tests/server/test_signup.py b/timApp/tests/server/test_signup.py index 9e54becd19..0c467e06ee 100644 --- a/timApp/tests/server/test_signup.py +++ b/timApp/tests/server/test_signup.py @@ -12,7 +12,7 @@ from timApp.messaging.messagelist.listinfo import Channel from timApp.tests.server.timroutetest import TimRouteTest from timApp.tim_app import get_home_organization_group, app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.newuser import NewUser from timApp.user.personaluniquecode import SchacPersonalUniqueCode from timApp.user.user import User, UserOrigin, UserInfo @@ -114,7 +114,7 @@ def test_block_bot_signup(self): ) self.get("/") # refresh session self.assertIsNone( - db.session.execute(select(NewUser).filter_by(email=bot_email).limit(1)) + run_sql(select(NewUser).filter_by(email=bot_email).limit(1)) .scalars() .first() ) @@ -129,20 +129,18 @@ def test_block_bot_signup(self): ) self.get("/") # refresh session self.assertIsNotNone( - db.session.execute( - select(NewUser).filter_by(email=allowed_email).limit(1) - ) + run_sql(select(NewUser).filter_by(email=allowed_email).limit(1)) .scalars() .first() ) - db.session.execute(delete(NewUser)) + run_sql(delete(NewUser)) db.session.commit() def test_signup_case_insensitive(self): email = "SomeOneCase@example.com" self.json_post("/emailSignup", {"email": email}) self.assertEqual( - db.session.execute(select(NewUser.email)).scalars().all(), + run_sql(select(NewUser.email)).scalars().all(), ["someonecase@example.com"], ) self.json_post( @@ -168,7 +166,7 @@ def test_signup_whitespace(self): email = "whitespace@example.com " self.json_post("/emailSignup", {"email": email}) self.assertEqual( - db.session.execute(select(NewUser.email)).scalars().all(), + run_sql(select(NewUser.email)).scalars().all(), ["whitespace@example.com"], ) self.json_post( @@ -218,9 +216,9 @@ def test_login_case_insensitive(self): def test_signup(self): email = "testingsignup@example.com" self.json_post("/emailSignup", {"email": email}) - self.assertEqual(db.session.execute(select(NewUser.email)).scalars().all(), [email]) + self.assertEqual(run_sql(select(NewUser.email)).scalars().all(), [email]) self.json_post("/emailSignup", {"email": email}) - self.assertEqual(db.session.execute(select(NewUser.email)).scalars().all(), [email]) + self.assertEqual(run_sql(select(NewUser.email)).scalars().all(), [email]) self.json_post( "/emailSignupFinish", { @@ -233,7 +231,7 @@ def test_signup(self): expect_contains="registered", json_key="status", ) - self.assertEqual(db.session.execute(select(NewUser.email)).scalars().all(), []) + self.assertEqual(run_sql(select(NewUser.email)).scalars().all(), []) self.assertEqual("Testing Signup", self.current_user.real_name) self.assertEqual(UserOrigin.Email, self.current_user.origin) self.assertEqual(email, self.current_user.email) diff --git a/timApp/tests/server/test_tim_message.py b/timApp/tests/server/test_tim_message.py index e8cbd722b1..f1d127facb 100644 --- a/timApp/tests/server/test_tim_message.py +++ b/timApp/tests/server/test_tim_message.py @@ -8,7 +8,7 @@ InternalMessage, ) from timApp.tests.server.timroutetest import TimRouteTest -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql class UrlTest(TimRouteTest): @@ -130,7 +130,7 @@ def test_send_message(self): ) # tim-messages folder created successfully display = ( - db.session.execute( + run_sql( select(InternalMessageDisplay) .filter_by(usergroup_id=self.get_test_user_1_group_id()) .limit(1) @@ -139,14 +139,12 @@ def test_send_message(self): .first() ) msg = ( - db.session.execute( - select(InternalMessage).filter_by(id=display.message_id).limit(1) - ) + run_sql(select(InternalMessage).filter_by(id=display.message_id).limit(1)) .scalars() .first() ) msg_doc = ( - db.session.execute(select(DocEntry).filter_by(id=msg.doc_id).limit(1)) + run_sql(select(DocEntry).filter_by(id=msg.doc_id).limit(1)) .scalars() .first() ) diff --git a/timApp/tests/server/test_translation.py b/timApp/tests/server/test_translation.py index 89ba77dbc0..6d79dc60e6 100644 --- a/timApp/tests/server/test_translation.py +++ b/timApp/tests/server/test_translation.py @@ -21,7 +21,7 @@ from timApp.tests.db.timdbtest import TimDbTest from timApp.tests.server.timroutetest import TimRouteTest from timApp.tim_app import app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.util.utils import static_tim_doc MAX_TEST_CHAR_QUOTA = 50 @@ -109,11 +109,7 @@ def usage(self) -> Usage: class TranslationTest(TimTranslationRouteTest): def get_deepl_service(self) -> DeeplTranslationService: - return ( - db.session.execute(select(DeeplTranslationService).limit(1)) - .scalars() - .first() - ) + return run_sql(select(DeeplTranslationService).limit(1)).scalars().first() def test_translation_create(self): self.login_test1() diff --git a/timApp/tests/server/test_user_sessions.py b/timApp/tests/server/test_user_sessions.py index 38179da630..01b9be33ff 100644 --- a/timApp/tests/server/test_user_sessions.py +++ b/timApp/tests/server/test_user_sessions.py @@ -4,7 +4,7 @@ from timApp.auth.session.model import UserSession from timApp.auth.session.util import verify_session_for from timApp.tests.server.timroutetest import TimRouteTest -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql class UserSessionsTest(TimRouteTest): @@ -16,7 +16,7 @@ def forget_session(self): def latest_session(self) -> UserSession: """Get latest session of Test User 1.""" return ( - db.session.execute( + run_sql( select(UserSession) .filter_by(user_id=self.test_user_1.id) .order_by(UserSession.logged_in_at.desc()) @@ -32,7 +32,7 @@ def assert_sesion_expired_state( """Assert the state of Test User 1's sessions.""" self.assertEqual( [ - db.session.execute( + run_sql( select(UserSession).filter_by( user_id=self.test_user_1.id, session_id=sess, @@ -56,12 +56,10 @@ def test_session_basic(self): "SESSIONS_MAX_CONCURRENT_SESSIONS_PER_USER": None, } ): - db.session.execute(delete(UserSession)) + run_sql(delete(UserSession)) db.session.commit() self.login_test1(manual=True) - sessions: list[UserSession] = ( - db.session.execute(select(UserSession)).scalars().all() - ) + sessions: list[UserSession] = run_sql(select(UserSession)).scalars().all() self.assertEqual(len(sessions), 1) self.assertEqual(sessions[0].user.name, self.test_user_1.name) self.assertEqual(sessions[0].expired, False) @@ -75,9 +73,7 @@ def test_session_basic(self): ) self.logout() - sessions: list[UserSession] = ( - db.session.execute(select(UserSession)).scalars().all() - ) + sessions: list[UserSession] = run_sql(select(UserSession)).scalars().all() self.assertEqual(len(sessions), 1) self.assertEqual(sessions[0].user.name, self.test_user_1.name) self.assertEqual(sessions[0].expired, True) @@ -96,7 +92,7 @@ def test_session_access_block(self): "SESSIONS_MAX_CONCURRENT_SESSIONS_PER_USER": 1, } ): - db.session.execute(delete(UserSession)) + run_sql(delete(UserSession)) db.session.commit() self.login_test1(manual=True) @@ -114,7 +110,7 @@ def test_session_validity(self): "SESSIONS_MAX_CONCURRENT_SESSIONS_PER_USER": 1, } ): - db.session.execute(delete(UserSession)) + run_sql(delete(UserSession)) db.session.commit() self.login_test1(manual=True) @@ -181,7 +177,7 @@ def test_session_validity(self): }, ) prev_session = ( - db.session.execute( + run_sql( select(UserSession) .filter_by(user_id=self.test_user_1.id, session_id=prev_id) .limit(1) @@ -206,7 +202,7 @@ def test_session_verify_remote(self): "DIST_RIGHTS_RECEIVE_SECRET": "yyy", } ): - db.session.execute(delete(UserSession)) + run_sql(delete(UserSession)) db.session.commit() session_ids = [] @@ -266,7 +262,7 @@ def test_session_verify_remote(self): ) # Mark all sessions as not expired to test verification of the latest session - db.session.execute( + run_sql( update(UserSession) .where(UserSession.user_id == self.test_user_1.id) .values({"expired_at": None}) @@ -297,7 +293,7 @@ def test_session_invalidate(self) -> None: "DIST_RIGHTS_RECEIVE_SECRET": "yyy", } ): - db.session.execute(delete(UserSession)) + run_sql(delete(UserSession)) db.session.commit() session_ids = [] @@ -370,7 +366,7 @@ def test_session_invalidate(self) -> None: for session_id in session_ids: sess = ( - db.session.execute( + run_sql( select(UserSession).filter_by(session_id=session_id).limit(1) ) .scalars() diff --git a/timApp/tests/server/test_velp.py b/timApp/tests/server/test_velp.py index 6c11572b7c..7d6e56afbf 100644 --- a/timApp/tests/server/test_velp.py +++ b/timApp/tests/server/test_velp.py @@ -23,7 +23,7 @@ from timApp.document.docinfo import DocInfo from timApp.folder.folder import Folder from timApp.tests.server.timroutetest import TimRouteTest -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.usergroup import UserGroup from timApp.util.utils import get_current_time from timApp.velp.annotation import Annotation @@ -366,34 +366,24 @@ def test_delete_velp_group(self): self.assertEqual(f"roskis/{g['name']}", deleted.path) # database should not contain any references to the velp group - vg = ( - db.session.execute(select(VelpGroup).filter_by(id=g["id"]).limit(1)) - .scalars() - .first() - ) + vg = run_sql(select(VelpGroup).filter_by(id=g["id"]).limit(1)).scalars().first() v_in_g = ( - db.session.execute(select(VelpInGroup).filter_by(velp_group_id=g["id"])) + run_sql(select(VelpInGroup).filter_by(velp_group_id=g["id"])) .scalars() .all() ) vg_sel = ( - db.session.execute( - select(VelpGroupSelection).filter_by(velp_group_id=g["id"]) - ) + run_sql(select(VelpGroupSelection).filter_by(velp_group_id=g["id"])) .scalars() .all() ) vg_def = ( - db.session.execute( - select(VelpGroupDefaults).filter_by(velp_group_id=g["id"]) - ) + run_sql(select(VelpGroupDefaults).filter_by(velp_group_id=g["id"])) .scalars() .all() ) vg_in_doc = ( - db.session.execute( - select(VelpGroupsInDocument).filter_by(velp_group_id=g["id"]) - ) + run_sql(select(VelpGroupsInDocument).filter_by(velp_group_id=g["id"])) .scalars() .all() ) @@ -416,33 +406,25 @@ def test_delete_velp_group(self): # database should not contain any references to the velp group vg2 = ( - db.session.execute(select(VelpGroup).filter_by(id=g2["id"]).limit(1)) - .scalars() - .first() + run_sql(select(VelpGroup).filter_by(id=g2["id"]).limit(1)).scalars().first() ) v_in_g2 = ( - db.session.execute(select(VelpInGroup).filter_by(velp_group_id=g2["id"])) + run_sql(select(VelpInGroup).filter_by(velp_group_id=g2["id"])) .scalars() .all() ) vg_sel2 = ( - db.session.execute( - select(VelpGroupSelection).filter_by(velp_group_id=g2["id"]) - ) + run_sql(select(VelpGroupSelection).filter_by(velp_group_id=g2["id"])) .scalars() .all() ) vg_def2 = ( - db.session.execute( - select(VelpGroupDefaults).filter_by(velp_group_id=g2["id"]) - ) + run_sql(select(VelpGroupDefaults).filter_by(velp_group_id=g2["id"])) .scalars() .all() ) vg_in_doc2 = ( - db.session.execute( - select(VelpGroupsInDocument).filter_by(velp_group_id=g2["id"]) - ) + run_sql(select(VelpGroupsInDocument).filter_by(velp_group_id=g2["id"])) .scalars() .all() ) diff --git a/timApp/tests/server/test_verification.py b/timApp/tests/server/test_verification.py index cb1b2562c8..65d2160db2 100644 --- a/timApp/tests/server/test_verification.py +++ b/timApp/tests/server/test_verification.py @@ -4,7 +4,7 @@ from timApp.notification.send_email import sent_mails_in_testing from timApp.tests.server.timroutetest import TimRouteTest from timApp.tim_app import app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.usercontact import UserContact from timApp.user.verification.verification import ContactAddVerification @@ -31,7 +31,7 @@ def add_email(email: str) -> tuple[dict, str]: self.assertIsNotNone(u.get_contact(Channel.EMAIL, email)) verification = ( - db.session.execute( + run_sql( select(ContactAddVerification) .join(UserContact) .filter( @@ -131,7 +131,7 @@ def test_custom_verify_template(self): ) verification = ( - db.session.execute( + run_sql( select(ContactAddVerification) .join(UserContact) .filter( diff --git a/timApp/tests/server/timroutetest.py b/timApp/tests/server/timroutetest.py index ecce15d3e4..405003a9d2 100644 --- a/timApp/tests/server/timroutetest.py +++ b/timApp/tests/server/timroutetest.py @@ -55,7 +55,7 @@ from timApp.readmark.readparagraphtype import ReadParagraphType from timApp.tests.db.timdbtest import TimDbTest from timApp.tim_app import app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.util.utils import remove_prefix @@ -1456,9 +1456,7 @@ def create_list( }, ) message_list: MessageListModel = ( - db.session.execute(select(MessageListModel).filter_by(name=name)) - .scalars() - .one() + run_sql(select(MessageListModel).filter_by(name=name)).scalars().one() ) return manage_doc, message_list diff --git a/timApp/tim_celery.py b/timApp/tim_celery.py index 7b6356b05b..8f4a0ec051 100644 --- a/timApp/tim_celery.py +++ b/timApp/tim_celery.py @@ -26,7 +26,7 @@ from timApp.plugin.plugin import Plugin from timApp.plugin.pluginexception import PluginException from timApp.tim_app import app -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.verification.verification import Verification from timApp.util.flask.search import create_search_files @@ -253,13 +253,13 @@ def cleanup_verifications(): now = get_current_time() end_time_unreacted = now - timedelta(seconds=max_unreacted_interval) end_time_reacted = now - timedelta(seconds=max_reacted_interval) - db.session.execute( + run_sql( delete(Verification).where( (Verification.requested_at < end_time_unreacted) & (Verification.reacted_at == None) ) ) - db.session.execute( + run_sql( delete(Verification).where( (Verification.requested_at < end_time_reacted) & (Verification.reacted_at != None) diff --git a/timApp/timdb/sqa.py b/timApp/timdb/sqa.py index 56c215d487..04af4b02ae 100644 --- a/timApp/timdb/sqa.py +++ b/timApp/timdb/sqa.py @@ -10,26 +10,20 @@ from typing import Optional from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import func, text +from sqlalchemy import func, text, Executable, Result from sqlalchemy.orm import mapped_column from sqlalchemy.orm.base import instance_state, Mapped from timApp.timdb.types import datetime_tz, DbModel -session_options = { - "future": True, -} -engine_options = { - "future": True, -} +session_options = {} + if os.environ.get("TIM_TESTING", None): # Disabling object expiration on commit makes testing easier # because sometimes objects would expire after calling a route. session_options["expire_on_commit"] = False -db = SQLAlchemy( - session_options=session_options, engine_options=engine_options, model_class=DbModel -) +db = SQLAlchemy(session_options=session_options, model_class=DbModel) # Overwrite metadata to use the DbModel's metadata # Flask-SQLAlchemy 3.x doesn't appear to have a correct handler of model_class, so it ends up overwriting our DbModel # Instead, we pass our model manually @@ -37,8 +31,6 @@ db.metadatas[None] = DbModel.metadata -# TODO: Replace DbModel with custom DeclarativeBase class that also specifies __tablename__ and custom types. -# See https://docs.sqlalchemy.org/en/20/orm/declarative_mixins.html # TODO: Switch models to use dataclasses instead # See https://docs.sqlalchemy.org/en/20/orm/dataclasses.html#declarative-dataclass-mapping # This should fix DeeplTranslationService's extra args, see https://docs.sqlalchemy.org/en/20/orm/dataclasses.html#using-non-mapped-dataclass-fields @@ -55,6 +47,20 @@ class TimeStampMixin: ) +def run_sql(statement: Executable, *args, **kwargs) -> Result: + """ + Runs a SQL statement and returns the result. + + The arguments are passed to SQLAlchemy's session.execute() function. + + :param statement: The SQL statement to run + :param args: Arguments to pass to session.execute() + :param kwargs: Arguments to pass to session.execute() + :return: Result of the SQL statement as a SQLAlchemy Result object that represents the returned rows. + """ + return db.session.execute(statement, *args, **kwargs) + + def tim_main_execute(sql: str, params=None): return db.session.execute( text(sql), params, bind_arguments={"bind": get_tim_main_engine()} diff --git a/timApp/upload/upload.py b/timApp/upload/upload.py index 59a50a7214..198a626e2e 100644 --- a/timApp/upload/upload.py +++ b/timApp/upload/upload.py @@ -41,7 +41,7 @@ from timApp.plugin.pluginexception import PluginException from timApp.plugin.taskid import TaskId, TaskIdAccess from timApp.timdb.dbaccess import get_files_path -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.upload.uploadedfile import ( PluginUpload, PluginUploadInfo, @@ -124,7 +124,7 @@ def get_pluginupload(relfilename: str) -> tuple[str, PluginUpload]: relfilename = check_and_format_filename(relfilename) block = ( - db.session.execute( + run_sql( select(Block) .filter( (Block.description.startswith(relfilename)) @@ -176,7 +176,7 @@ def get_multiple_pluginuploads(relfilenames: list[str]) -> list[PluginUpload]: value=Block.description, ) blocks: list[Block] = ( - db.session.execute( + run_sql( select(Block) .filter( (Block.description.in_(filenames)) diff --git a/timApp/upload/uploadedfile.py b/timApp/upload/uploadedfile.py index 06987f5ca2..bf040c679d 100644 --- a/timApp/upload/uploadedfile.py +++ b/timApp/upload/uploadedfile.py @@ -15,7 +15,7 @@ from timApp.item.item import ItemBase from timApp.timdb.dbaccess import get_files_path from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User DIR_MAPPING = { @@ -56,7 +56,7 @@ def find_by_id(block_id: int) -> Optional["UploadedFile"]: @staticmethod def find_first_child(block: Block, name: str) -> Optional["UploadedFile"]: b = ( - db.session.execute( + run_sql( select(Block) .join(BlockAssociation, BlockAssociation.child == Block.id) .filter( diff --git a/timApp/user/contacts.py b/timApp/user/contacts.py index 8749432d6c..bfad7b1f93 100644 --- a/timApp/user/contacts.py +++ b/timApp/user/contacts.py @@ -7,7 +7,7 @@ from timApp.auth.accesshelper import verify_logged_in from timApp.auth.sessioninfo import get_current_user_object from timApp.messaging.messagelist.listinfo import Channel -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.usercontact import UserContact, ContactOrigin, PrimaryContact from timApp.user.verification.verification import ( resend_verification, @@ -54,7 +54,7 @@ def add_contact( "The contact is already added but is pending verification" ) verification = ( - db.session.execute( + run_sql( select(ContactAddVerification) .filter_by(contact=existing_contact_info, reacted_at=None) .limit(1) @@ -173,7 +173,7 @@ def set_primary( json_response({"verify": False}) primary_contact_exists = ( - db.session.execute( + run_sql( select(UserContact.id) .filter_by(channel=channel, contact=contact, primary=PrimaryContact.true) .limit(1) @@ -189,7 +189,7 @@ def set_primary( ) existing_verification = ( - db.session.execute( + run_sql( select(SetPrimaryContactVerification).filter_by( contact=existing_contact, reacted_at=None, diff --git a/timApp/user/groups.py b/timApp/user/groups.py index b61410c82a..412f9d96cb 100644 --- a/timApp/user/groups.py +++ b/timApp/user/groups.py @@ -20,7 +20,7 @@ from timApp.document.create_item import apply_template, create_document from timApp.document.docinfo import DocInfo from timApp.item.validation import ItemValidationRule -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.special_group_names import ( SPECIAL_GROUPS, PRIVILEGED_GROUPS, @@ -64,7 +64,7 @@ def get_uid_gid( group_name: str, usernames_or_emails: list[str] ) -> tuple[UserGroup, list[User]]: users = ( - db.session.execute( + run_sql( select(User).filter( User.name.in_(usernames_or_emails) | User.email.in_(usernames_or_emails) ) @@ -73,9 +73,7 @@ def get_uid_gid( .all() ) group = ( - db.session.execute(select(UserGroup).filter_by(name=group_name).limit(1)) - .scalars() - .first() + run_sql(select(UserGroup).filter_by(name=group_name).limit(1)).scalars().first() ) raise_group_not_found_if_none(group_name, group) return group, users @@ -139,7 +137,7 @@ def show_usergroups(username: str) -> Response: if not u: raise NotExist(USER_NOT_FOUND) return json_response( - db.session.execute(u.get_groups(include_special=False).order_by(UserGroup.name)) + run_sql(u.get_groups(include_special=False).order_by(UserGroup.name)) .scalars() .all() ) diff --git a/timApp/user/hakaorganization.py b/timApp/user/hakaorganization.py index 432bb0faa7..04f7c6795b 100644 --- a/timApp/user/hakaorganization.py +++ b/timApp/user/hakaorganization.py @@ -5,7 +5,7 @@ from sqlalchemy import select from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import DbModel if TYPE_CHECKING: @@ -25,7 +25,7 @@ class HakaOrganization(DbModel): @staticmethod def get_or_create(name: str): found = ( - db.session.execute(select(HakaOrganization).filter_by(name=name).limit(1)) + run_sql(select(HakaOrganization).filter_by(name=name).limit(1)) .scalars() .first() ) diff --git a/timApp/user/personaluniquecode.py b/timApp/user/personaluniquecode.py index 9b14a5e06d..1e75dbe8d5 100644 --- a/timApp/user/personaluniquecode.py +++ b/timApp/user/personaluniquecode.py @@ -5,7 +5,7 @@ from sqlalchemy import select, UniqueConstraint, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.timdb.types import DbModel from timApp.user.hakaorganization import HakaOrganization @@ -52,7 +52,7 @@ def find_by_code( code: str, org: str, codetype: str ) -> Optional["PersonalUniqueCode"]: return ( - db.session.execute( + run_sql( select(PersonalUniqueCode) .filter_by(code=code, type=codetype) .join(HakaOrganization) diff --git a/timApp/user/preferences.py b/timApp/user/preferences.py index 7ab1062ad2..b0934c8891 100644 --- a/timApp/user/preferences.py +++ b/timApp/user/preferences.py @@ -9,7 +9,7 @@ from timApp.document.docentry import DocEntry from timApp.item.item import Item -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.settings.style_utils import resolve_themes from tim_common.html_sanitize import sanitize_css @@ -45,9 +45,7 @@ def theme_docs(self) -> list[DocEntry]: return [] ordering = {d: i for i, d in enumerate(self.style_doc_ids)} return sorted( - db.session.execute( - select(DocEntry).filter(DocEntry.id.in_(self.style_doc_ids)) - ) + run_sql(select(DocEntry).filter(DocEntry.id.in_(self.style_doc_ids))) .scalars() .all(), key=lambda d: ordering[d.id], diff --git a/timApp/user/settings/settings.py b/timApp/user/settings/settings.py index a92a85b858..a66d5fe6a3 100644 --- a/timApp/user/settings/settings.py +++ b/timApp/user/settings/settings.py @@ -16,7 +16,7 @@ from timApp.folder.folder import Folder from timApp.item.block import Block, BlockType from timApp.notification.notify import get_current_user_notifications -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.consentchange import ConsentChange from timApp.user.preferences import Preferences from timApp.user.settings.style_utils import is_style_doc @@ -58,7 +58,7 @@ def verify_new_styles(curr_prefs: Preferences, new_prefs: Preferences) -> None: return new_style_docs: list[DocEntry] = ( - db.session.execute(select(DocEntry).filter(DocEntry.id.in_(new_style_doc_ids))) + run_sql(select(DocEntry).filter(DocEntry.id.in_(new_style_doc_ids))) .scalars() .all() ) @@ -122,17 +122,11 @@ def get_user_info(u: User, include_doc_content: bool = False) -> dict[str, Any]: """Returns all data associated with a user.""" block_query = get_owned_objects_query(u) docs = ( - db.session.execute(select(DocEntry).filter(DocEntry.id.in_(block_query))) - .scalars() - .all() - ) - folders = ( - db.session.execute(select(Folder).filter(Folder.id.in_(block_query))) - .scalars() - .all() + run_sql(select(DocEntry).filter(DocEntry.id.in_(block_query))).scalars().all() ) + folders = run_sql(select(Folder).filter(Folder.id.in_(block_query))).scalars().all() images = ( - db.session.execute( + run_sql( select(Block).filter( Block.id.in_(block_query) & (Block.type_id == BlockType.Image.value) ) @@ -141,7 +135,7 @@ def get_user_info(u: User, include_doc_content: bool = False) -> dict[str, Any]: .all() ) files = ( - db.session.execute( + run_sql( select(Block).filter( Block.id.in_(block_query) & (Block.type_id == BlockType.File.value) ) @@ -151,7 +145,7 @@ def get_user_info(u: User, include_doc_content: bool = False) -> dict[str, Any]: ) answers = u.answers.all() answer_uploads = ( - db.session.execute( + run_sql( select(AnswerUpload).filter( AnswerUpload.answer_id.in_([a.id for a in answers]) ) diff --git a/timApp/user/settings/styles.py b/timApp/user/settings/styles.py index ee22ff486c..23a43c4075 100644 --- a/timApp/user/settings/styles.py +++ b/timApp/user/settings/styles.py @@ -19,7 +19,7 @@ from timApp.document.usercontext import UserContext from timApp.document.viewcontext import default_view_ctx from timApp.item.partitioning import get_doc_version_hash -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.settings.style_utils import ( stylesheets_folder, get_default_scss_gen_dir, @@ -369,9 +369,7 @@ def generate( verify_logged_in() doc_entries: list[DocEntry] = ( - db.session.execute(select(DocEntry).filter(DocEntry.id.in_(docs))) - .scalars() - .all() + run_sql(select(DocEntry).filter(DocEntry.id.in_(docs))).scalars().all() ) for doc in doc_entries: diff --git a/timApp/user/user.py b/timApp/user/user.py index 7a429e5555..c00157f383 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -40,7 +40,7 @@ from timApp.notification.notification import Notification, NotificationType from timApp.sisu.scimusergroup import ScimUserGroup from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db, TimeStampMixin, is_attribute_loaded +from timApp.timdb.sqa import db, TimeStampMixin, is_attribute_loaded, run_sql from timApp.timdb.types import DbModel from timApp.user.hakaorganization import HakaOrganization, get_home_organization_id from timApp.user.personaluniquecode import SchacPersonalUniqueCode, PersonalUniqueCode @@ -502,7 +502,7 @@ def update_email( if prev_email != new_email: if create_contact: new_primary = ( - db.session.execute( + run_sql( select(UserContact) .filter_by( user_id=self.id, channel=Channel.EMAIL, contact=new_email @@ -562,7 +562,7 @@ def scim_extra_data(self): ) return { "emails": [ - {"value": uc.contact} for uc in db.session.scalars(email_contacts_stmt) + {"value": uc.contact} for uc in run_sql(email_contacts_stmt).scalars() ] } @@ -611,9 +611,11 @@ def effective_real_groups(): locked_groups = get_locked_active_groups() if locked_groups is None: return effective_real_groups() - return db.session.scalars( - select(UserGroup).filter(UserGroup.id.in_(locked_groups)) - ).all() + return ( + run_sql(select(UserGroup).filter(UserGroup.id.in_(locked_groups))) + .scalars() + .all() + ) @property def effective_group_ids(self): @@ -711,9 +713,11 @@ def create_with_group( @staticmethod def get_by_name(name: str) -> Optional["User"]: - return db.session.scalars( - user_query_with_joined_groups().filter_by(name=name).limit(1) - ).first() + return ( + run_sql(user_query_with_joined_groups().filter_by(name=name).limit(1)) + .scalars() + .first() + ) @staticmethod def get_by_id(uid: int) -> Optional["User"]: @@ -724,18 +728,22 @@ def get_by_email(email: str) -> Optional["User"]: if email is None: raise Exception("Tried to find an user by null email") return ( - db.session.execute( - user_query_with_joined_groups().filter_by(email=email).limit(1) - ) + run_sql(user_query_with_joined_groups().filter_by(email=email).limit(1)) .scalars() .first() ) @staticmethod def get_by_email_case_insensitive(email: str) -> list["User"]: - return db.session.scalars( - user_query_with_joined_groups().filter(func.lower(User.email).in_([email])) - ).all() + return ( + run_sql( + user_query_with_joined_groups().filter( + func.lower(User.email).in_([email]) + ) + ) + .scalars() + .all() + ) @staticmethod def get_by_email_case_insensitive_or_username( @@ -753,11 +761,11 @@ def get_by_email_case_insensitive_or_username( def verified_email_name_parts(self) -> list[str]: email_parts = [ uc.contact.split("@") - for uc in db.session.scalars( + for uc in run_sql( select(UserContact).filter_by( user=self, channel=Channel.EMAIL, verified=True ) - ) + ).scalars() ] return [parts[0].lower() for parts in email_parts] @@ -845,7 +853,7 @@ def _get_personal_folders(self) -> list[Folder]: ) ) - return db.session.scalars(stmt).all() + return run_sql(stmt).scalars().all() @cached_property def personal_folder_prop(self) -> Folder: @@ -956,7 +964,7 @@ def get_contact( ) if options: stmt = stmt.options(*options) - return db.session.scalars(stmt).one_or_none() + return run_sql(stmt).scalars().one_or_none() @staticmethod def get_scimuser() -> "User": @@ -1371,7 +1379,7 @@ def remove_access(self, block_id: int, access_type: str | AccessType) -> None: & (BlockAccess.usergroup_id == self.get_personal_group().id) & (BlockAccess.type == get_access_type_id(access_type)) ) - db.session.execute(stmt) + run_sql(stmt) def get_notify_settings(self, item: DocInfo | Folder) -> dict: # TODO: Instead of conversion, expose all notification types in UI @@ -1474,7 +1482,7 @@ def is_sisu_teacher(self) -> bool: if self.is_special: return False teacher_group_id = ( - db.session.execute( + run_sql( select(ScimUserGroup.group_id) .join(UserGroup) .join(UserGroupMember) @@ -1521,11 +1529,13 @@ def to_json(self, full: bool = False, contacts: bool = False) -> dict: external_ids: dict[int, str] = ( { s.group_id: s.external_id - for s in db.session.scalars( + for s in run_sql( select(ScimUserGroup).filter( ScimUserGroup.group_id.in_([g.id for g in self.groups]) ) - ).all() + ) + .scalars() + .all() } if full else [] diff --git a/timApp/user/usergroup.py b/timApp/user/usergroup.py index cfadfb003e..0faed0400f 100644 --- a/timApp/user/usergroup.py +++ b/timApp/user/usergroup.py @@ -17,7 +17,13 @@ from timApp.sisu.parse_display_name import parse_sisu_group_display_name from timApp.sisu.scimusergroup import ScimUserGroup -from timApp.timdb.sqa import db, TimeStampMixin, include_if_exists, is_attribute_loaded +from timApp.timdb.sqa import ( + db, + TimeStampMixin, + include_if_exists, + is_attribute_loaded, + run_sql, +) from timApp.timdb.types import DbModel from timApp.user.scimentity import SCIMEntity from timApp.user.special_group_names import ( @@ -238,15 +244,13 @@ def get_by_external_id(name: str) -> UserGroup: @staticmethod def get_by_name(name) -> UserGroup: return ( - db.session.execute(select(UserGroup).filter_by(name=name).limit(1)) - .scalars() - .first() + run_sql(select(UserGroup).filter_by(name=name).limit(1)).scalars().first() ) @staticmethod def get_anonymous_group() -> UserGroup: return ( - db.session.execute(select(UserGroup).filter_by(name=ANONYMOUS_GROUPNAME)) + run_sql(select(UserGroup).filter_by(name=ANONYMOUS_GROUPNAME)) .scalars() .one() ) @@ -254,15 +258,13 @@ def get_anonymous_group() -> UserGroup: @staticmethod def get_admin_group() -> UserGroup: return ( - db.session.execute(select(UserGroup).filter_by(name=ADMIN_GROUPNAME)) - .scalars() - .one() + run_sql(select(UserGroup).filter_by(name=ADMIN_GROUPNAME)).scalars().one() ) @staticmethod def get_groupadmin_group() -> UserGroup: return ( - db.session.execute(select(UserGroup).filter_by(name=GROUPADMIN_GROUPNAME)) + run_sql(select(UserGroup).filter_by(name=GROUPADMIN_GROUPNAME)) .scalars() .one() ) @@ -280,7 +282,7 @@ def get_haka_group() -> UserGroup: @staticmethod def get_organizations() -> list[UserGroup]: return ( - db.session.execute( + run_sql( select(UserGroup).filter( UserGroup.name.endswith(" users") & UserGroup.name.notin_(SPECIAL_GROUPS) @@ -293,7 +295,7 @@ def get_organizations() -> list[UserGroup]: @staticmethod def get_teachers_group() -> UserGroup: return ( - db.session.execute(select(UserGroup).filter_by(name=TEACHERS_GROUPNAME)) + run_sql(select(UserGroup).filter_by(name=TEACHERS_GROUPNAME)) .scalars() .one() ) @@ -318,7 +320,7 @@ def get_or_create_group(group_name: str) -> UserGroup: @staticmethod def get_logged_in_group() -> UserGroup: return ( - db.session.execute(select(UserGroup).filter_by(name=LOGGED_IN_GROUPNAME)) + run_sql(select(UserGroup).filter_by(name=LOGGED_IN_GROUPNAME)) .scalars() .one() ) @@ -351,7 +353,7 @@ def get_usergroup_eager_query() -> Select: def get_sisu_groups_by_filter(f) -> list[UserGroup]: gs: list[UserGroup] = ( - db.session.execute(get_usergroup_eager_query().join(ScimUserGroup).filter(f)) + run_sql(get_usergroup_eager_query().join(ScimUserGroup).filter(f)) .scalars() .all() ) diff --git a/timApp/user/users.py b/timApp/user/users.py index eda6de99ea..cc5568e9c4 100644 --- a/timApp/user/users.py +++ b/timApp/user/users.py @@ -1,13 +1,14 @@ from collections import defaultdict +from typing import Sequence -from sqlalchemy import func, select +from sqlalchemy import func, select, Row from sqlalchemy.orm import selectinload from timApp.auth.accesstype import AccessType from timApp.auth.auth_models import BlockAccess from timApp.folder.folder import Folder from timApp.item.block import Block, BlockType -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.special_group_names import ( ANONYMOUS_USERNAME, ANONYMOUS_GROUPNAME, @@ -40,7 +41,7 @@ def get_rights_holders(block_id: int) -> RightsList: def get_rights_holders_all(block_ids: list[int], order_by=None): if not order_by: order_by = User.name - result: list[tuple[BlockAccess, UserGroup, User | None]] = db.session.execute( + result: Sequence[Row[BlockAccess, UserGroup, User | None]] = run_sql( select(BlockAccess) .options( selectinload(BlockAccess.usergroup) diff --git a/timApp/user/userutils.py b/timApp/user/userutils.py index a4c4c2053f..f2c1fc3a8a 100644 --- a/timApp/user/userutils.py +++ b/timApp/user/userutils.py @@ -13,7 +13,7 @@ from timApp.item.block import BlockType, Block from timApp.item.item import ItemBase from timApp.timdb.exceptions import TimDbException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.special_group_names import ANONYMOUS_GROUPNAME, ANONYMOUS_USERNAME from timApp.user.usergroup import UserGroup from timApp.util.utils import get_current_time @@ -65,7 +65,7 @@ def get_anon_user_id() -> int: def get_access_type_id(access_type: str) -> int: if not access_type_map: - result = db.session.execute(select(AccessTypeModel)).scalars().all() + result = run_sql(select(AccessTypeModel)).scalars().all() for row in result: access_type_map[row.name] = row.id return access_type_map[access_type] @@ -90,7 +90,7 @@ def expire_access( :return: The BlockAccess object if there was previous access. Also returns whether the access was expired before. """ ba: BlockAccess | None = ( - db.session.execute( + run_sql( select(BlockAccess) .filter_by( type=access_type.value, diff --git a/timApp/user/verification/routes.py b/timApp/user/verification/routes.py index 7ebad67e2f..a5cc7a66a0 100644 --- a/timApp/user/verification/routes.py +++ b/timApp/user/verification/routes.py @@ -4,7 +4,7 @@ from timApp.auth.accesshelper import verify_logged_in from timApp.auth.sessioninfo import get_current_user_id -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.verification.verification import Verification, VerificationType from timApp.util.flask.requesthelper import RouteException from timApp.util.flask.responsehelper import json_response @@ -24,7 +24,7 @@ def get_verification_data( error = "Invalid verification type" verification: Verification | None = ( - db.session.execute( + run_sql( select(Verification) .filter_by(token=verify_token, type=verify_type_parsed) .limit(1) diff --git a/timApp/user/verification/verification.py b/timApp/user/verification/verification.py index b630c5b5a0..3ec61f6497 100644 --- a/timApp/user/verification/verification.py +++ b/timApp/user/verification/verification.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import load_only, mapped_column, Mapped, relationship from timApp.document.docentry import DocEntry -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.timdb.types import datetime_tz, DbModel from timApp.user.user import User from timApp.user.usercontact import UserContact, PrimaryContact @@ -126,7 +126,7 @@ def approve(self) -> None: if not self.contact: return current_primary = ( - db.session.execute( + run_sql( select(UserContact) .filter_by( user_id=self.user_id, @@ -147,7 +147,7 @@ def approve(self) -> None: # We update email directly since we already resolved the contact in previous steps u = ( - db.session.execute( + run_sql( select(User) .filter_by(id=self.user_id) .options(load_only(User.email, User.id)) diff --git a/timApp/util/flask/search.py b/timApp/util/flask/search.py index eb2f41e09c..cf2d76973e 100644 --- a/timApp/util/flask/search.py +++ b/timApp/util/flask/search.py @@ -24,7 +24,7 @@ from timApp.item.routes import get_document_relevance from timApp.timdb.dbaccess import get_files_path from timApp.timdb.exceptions import InvalidReferenceException -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.user import User from timApp.util.flask.requesthelper import ( get_option, @@ -67,7 +67,7 @@ def get_subfolders(m: GetFoldersModel): root_path = m.folder if root_path == "": return json_response([]) - folders = db.session.execute( + folders = run_sql( select(Folder).filter(Folder.location.like(root_path + "%")).limit(50) ).scalars() folders_viewable = [root_path] @@ -731,7 +731,7 @@ def fetch_search_items(search_items: dict, search_folder: str) -> list[DocInfo]: :return: list of DocInfo objects """ doc_infos: list[DocInfo] = ( - db.session.execute( + run_sql( select(DocEntry) .filter( (DocEntry.id.in_(search_items.keys())) diff --git a/timApp/util/get_fields.py b/timApp/util/get_fields.py index 023847e387..7b758779ff 100644 --- a/timApp/util/get_fields.py +++ b/timApp/util/get_fields.py @@ -28,7 +28,7 @@ from timApp.document.viewcontext import ViewContext from timApp.plugin.plugin import find_task_ids, CachedPluginFinder from timApp.plugin.taskid import TaskId -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.groups import verify_group_view_access from timApp.user.user import User, get_membership_end, get_membership_added from timApp.user.usergroup import UserGroup @@ -132,9 +132,7 @@ class RequestedGroups: @staticmethod def from_name_list(group_names: list[str]) -> "RequestedGroups": return RequestedGroups( - groups=db.session.execute( - select(UserGroup).filter(UserGroup.name.in_(group_names)) - ) + groups=run_sql(select(UserGroup).filter(UserGroup.name.in_(group_names))) .scalars() .all(), include_all_answered=ALL_ANSWERED_WILDCARD in group_names, @@ -358,7 +356,7 @@ def get_fields_and_users( elif user_filter is not None: # Ensure user filter gets applied even if group filter is skipped in include_all_answered q = q.filter(user_filter) - sub += db.session.execute( + sub += run_sql( q.group_by(Answer.task_id, User.id).with_only_columns( func.max(Answer.id), User.id ) @@ -385,13 +383,13 @@ def get_fields_and_users( q = q.with_only_columns(User).order_by(User.id).options(lazyload(User.groups)) if member_filter_type != MembershipFilter.Current: q = q.options(selectinload(User.memberships)) - users: list[User] = db.session.execute(q).scalars().all() + users: list[User] = run_sql(q).scalars().all() user_map = {} for u in users: user_map[u.id] = u global_taskids = [t for t in task_ids if t.is_global] global_answer_ids = ( - db.session.execute( + run_sql( valid_answers_query(global_taskids) .group_by(Answer.task_id) .with_only_columns(func.max(Answer.id)) @@ -400,7 +398,7 @@ def get_fields_and_users( .all() ) answs = ( - db.session.execute( + run_sql( select(Answer).filter( Answer.id.in_( itertools.chain((aid for aid, _ in sub), global_answer_ids) @@ -429,7 +427,7 @@ def get_fields_and_users( for u in users: counts[u.id] = {} cnt = func.count(Answer.id).label("cnt") - answer_counts = db.session.execute( + answer_counts = run_sql( select(Answer) .filter( Answer.task_id.in_([tid.doc_task for tid in tasks_with_count_field]) @@ -563,7 +561,7 @@ def get_tally_field_values( pts = get_points_by_rule( rule=psr, task_ids=tids, - user_ids=db.session.execute( + user_ids=run_sql( select(User.id).join(UserGroup, join_relation).filter(group_filter) ) .scalars() diff --git a/timApp/velp/annotation.py b/timApp/velp/annotation.py index 8f1228346b..0feac1a80f 100644 --- a/timApp/velp/annotation.py +++ b/timApp/velp/annotation.py @@ -33,7 +33,7 @@ get_all_reviews, get_reviews_related_to_user, ) -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User, has_no_higher_right from timApp.util.flask.requesthelper import RouteException from timApp.util.flask.responsehelper import ( @@ -226,7 +226,7 @@ def invalidate_annotation(id: int) -> Response: def get_annotation_or_abort(ann_id: int) -> Annotation: # Possibly bug: We need to create a new query object, otherwise raiseload() seems to pollute User's relations ann = ( - db.session.execute( + run_sql( set_annotation_query_opts(select(Annotation).filter_by(id=ann_id)).limit(1) ) .scalars() diff --git a/timApp/velp/annotations.py b/timApp/velp/annotations.py index 3244b056ba..fee356725b 100644 --- a/timApp/velp/annotations.py +++ b/timApp/velp/annotations.py @@ -20,7 +20,7 @@ get_reviews_where_user_is_reviewer_query, is_peerreview_enabled, ) -from timApp.timdb.sqa import db +from timApp.timdb.sqa import run_sql from timApp.user.user import User from timApp.velp.annotation_model import Annotation from timApp.velp.velp_models import VelpContent, VelpVersion, Velp, AnnotationComment @@ -97,7 +97,7 @@ def get_annotations_with_comments_in_document( .options(joinedload(Annotation.answer).selectinload(Answer.users_all)) .with_only_columns(Annotation) ) - anns = db.session.execute(q).scalars().all() + anns = run_sql(q).scalars().all() return anns diff --git a/timApp/velp/velp.py b/timApp/velp/velp.py index eb07d0936a..215222fc86 100644 --- a/timApp/velp/velp.py +++ b/timApp/velp/velp.py @@ -38,7 +38,7 @@ from timApp.item.deleting import ( soft_delete_document, ) -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.user.users import get_rights_holders, remove_access @@ -170,7 +170,7 @@ def get_default_velp_group(doc_id: int) -> Response: # if has_view_access(user_id, timdb.documents.get_document_id(v['name'])): velp_groups.append(v.id) def_velp_groups: list[VelpGroup] = ( - db.session.execute( + run_sql( select(VelpGroup).filter( VelpGroup.id.in_(velp_groups) & VelpGroup.default_group == True ) @@ -212,7 +212,7 @@ def get_default_personal_velp_group() -> Response: for v in found_velp_groups: velp_groups.append(v.id) default_group = ( - db.session.execute( + run_sql( select(VelpGroup) .filter(VelpGroup.id.in_(velp_groups) & VelpGroup.default_group == True) .limit(1) @@ -376,9 +376,7 @@ def add_velp() -> Response: # Check where user has edit rights and only add new velp to those velp_groups: list[VelpGroup] = [ vg - for vg in db.session.execute( - select(VelpGroup).filter(VelpGroup.id.in_(velp_group_ids)) - ) + for vg in run_sql(select(VelpGroup).filter(VelpGroup.id.in_(velp_group_ids))) .scalars() .all() if has_edit_access(vg.block) @@ -471,9 +469,7 @@ def update_velp_route(doc_id: int) -> Response: # Check that user has edit access to velp groups in given velp group list and add them to an add list groups_to_add: list[VelpGroup] = [ vg - for vg in db.session.execute( - select(VelpGroup).filter(VelpGroup.id.in_(velp_group_ids)) - ) + for vg in run_sql(select(VelpGroup).filter(VelpGroup.id.in_(velp_group_ids))) .scalars() .all() if has_edit_access(vg.block) @@ -560,7 +556,7 @@ def update_velp_label_route() -> Response: language_id = "FI" if language_id is None else language_id vlc: VelpLabelContent | None = ( - db.session.execute( + run_sql( select(VelpLabelContent) .filter_by( language_id=language_id, @@ -691,7 +687,7 @@ def reset_target_area_selections_to_defaults(doc_id: int) -> Response: user_id = get_current_user_id() - db.session.execute( + run_sql( delete(VelpGroupSelection).where( (VelpGroupSelection.doc_id == doc_id) & (VelpGroupSelection.user_id == user_id) @@ -711,7 +707,7 @@ def reset_all_selections_to_defaults(doc_id: int) -> Response: user_id = get_current_user_id() - db.session.execute( + run_sql( delete(VelpGroupSelection).filter_by( (VelpGroupSelection.doc_id == doc_id) & (VelpGroupSelection.user_id == user_id) @@ -917,27 +913,27 @@ def delete_velp_group(group_id: int) -> Response: remove_velp_group_perms(group_id) # Delete associated entries/rows from database - db.session.execute( + run_sql( delete(VelpInGroup) .where(VelpInGroup.velp_group_id == group_id) .execution_options(synchronize_session=False) ) - db.session.execute( + run_sql( delete(VelpGroupSelection) .where(VelpGroupSelection.velp_group_id == group_id) .execution_options(synchronize_session=False) ) - db.session.execute( + run_sql( delete(VelpGroupDefaults) .where(VelpGroupDefaults.velp_group_id == group_id) .execution_options(synchronize_session=False) ) - db.session.execute( + run_sql( delete(VelpGroupsInDocument) .where(VelpGroupsInDocument.velp_group_id == group_id) .execution_options(synchronize_session=False) ) - db.session.execute( + run_sql( delete(VelpGroup) .where(VelpGroup.id == group_id) .execution_options(synchronize_session=False) diff --git a/timApp/velp/velpgroups.py b/timApp/velp/velpgroups.py index 0d7f303757..087ac90f8b 100644 --- a/timApp/velp/velpgroups.py +++ b/timApp/velp/velpgroups.py @@ -17,7 +17,7 @@ from timApp.auth.accesstype import AccessType from timApp.document.docentry import DocEntry from timApp.document.docinfo import DocInfo -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.user.user import User from timApp.user.usergroup import UserGroup from timApp.user.users import get_rights_holders @@ -200,7 +200,7 @@ def get_groups_from_document_table(doc_id: int, user_id: int | None) -> list[Vel """ if not user_id: return ( - db.session.execute( + run_sql( select(VelpGroupsInDocument) .filter_by(doc_id=doc_id) .join(VelpGroup) @@ -210,7 +210,7 @@ def get_groups_from_document_table(doc_id: int, user_id: int | None) -> list[Vel .all() ) return ( - db.session.execute( + run_sql( select(VelpGroupsInDocument) .filter_by(user_id=user_id, doc_id=doc_id) .join(VelpGroup) @@ -257,9 +257,7 @@ def add_groups_to_document( ) -> None: """Adds velp groups to VelpGroupsInDocument table.""" existing: list[VelpGroupsInDocument] = ( - db.session.execute( - select(VelpGroupsInDocument).filter_by(user_id=user.id, doc_id=doc.id) - ) + run_sql(select(VelpGroupsInDocument).filter_by(user_id=user.id, doc_id=doc.id)) .scalars() .all() ) @@ -292,7 +290,7 @@ def change_selection( """ vgs: VelpGroupSelection | None = ( - db.session.execute( + run_sql( select(VelpGroupSelection) .filter_by( user_id=user_id, @@ -332,7 +330,7 @@ def change_all_target_area_default_selections( :param selected: True or False """ - db.session.execute( + run_sql( delete(VelpGroupDefaults).where( (VelpGroupDefaults.doc_id == doc_id) & (VelpGroupDefaults.target_type == target_type) @@ -340,9 +338,7 @@ def change_all_target_area_default_selections( ) ) vgids: list[VelpGroupsInDocument] = ( - db.session.execute( - select(VelpGroupsInDocument).filter_by(doc_id=doc_id, user_id=user_id) - ) + run_sql(select(VelpGroupsInDocument).filter_by(doc_id=doc_id, user_id=user_id)) .scalars() .all() ) @@ -371,7 +367,7 @@ def change_all_target_area_selections( """ if target_type == 0: for vgs in ( - db.session.execute( + run_sql( select(VelpGroupSelection).filter_by( doc_id=doc_id, target_id=target_id, user_id=user_id ) @@ -381,7 +377,7 @@ def change_all_target_area_selections( ): vgs.selected = selected elif target_type == 1: - db.session.execute( + run_sql( delete(VelpGroupSelection).where( (VelpGroupSelection.doc_id == doc_id) & (VelpGroupSelection.target_id == target_id) @@ -392,7 +388,7 @@ def change_all_target_area_selections( # target_type is 0 because only 0 always contains all velp groups user has access to. # Other target types will get added to database only after they've been clicked once in interface. vgss: list[VelpGroupSelection] = ( - db.session.execute( + run_sql( select(VelpGroupSelection).filter_by( doc_id=doc_id, user_id=user_id, @@ -427,7 +423,7 @@ def change_default_selection( """ vgd: VelpGroupDefaults = ( - db.session.execute( + run_sql( select(VelpGroupDefaults) .filter_by( doc_id=doc_id, @@ -462,7 +458,7 @@ def add_groups_to_selection_table( ) -> None: """Adds velp groups to VelpGroupSelection table.""" vgs = ( - db.session.execute( + run_sql( select(VelpGroupSelection) .filter_by( user_id=user_id, @@ -521,7 +517,7 @@ def get_personal_selections_for_velp_groups( """ vgss = ( - db.session.execute( + run_sql( select(VelpGroupSelection) .filter_by(doc_id=doc_id, user_id=user_id) .order_by(VelpGroupSelection.target_id) @@ -542,7 +538,7 @@ def get_default_selections_for_velp_groups( """ vgds = ( - db.session.execute( + run_sql( select(VelpGroupDefaults) .filter_by(doc_id=doc_id) .order_by(VelpGroupDefaults.target_id) diff --git a/timApp/velp/velps.py b/timApp/velp/velps.py index 7bfb6accaf..c6689559eb 100644 --- a/timApp/velp/velps.py +++ b/timApp/velp/velps.py @@ -12,7 +12,7 @@ from sqlalchemy import func, delete, select from sqlalchemy.orm import selectinload -from timApp.timdb.sqa import db +from timApp.timdb.sqa import db, run_sql from timApp.velp.velp_models import ( Velp, VelpVersion, @@ -183,7 +183,7 @@ def update_velp_labels(velp_id: int, labels: Iterable[int]) -> None: """ # First nuke existing labels. - db.session.execute(delete(LabelInVelp).where(LabelInVelp.velp_id == velp_id)) + run_sql(delete(LabelInVelp).where(LabelInVelp.velp_id == velp_id)) # Then add the new ones. add_labels_to_velp(velp_id, labels) @@ -199,7 +199,7 @@ def get_latest_velp_version( """ return ( - db.session.execute( + run_sql( select(VelpContent) .filter_by(language_id=language_id) .join(VelpVersion) @@ -254,7 +254,7 @@ def get_velp_content_for_document( .options(selectinload(Velp.groups).raiseload(VelpGroup.block)) .options(selectinload(Velp.velp_versions).joinedload(VelpVersion.content)) ) - return db.session.execute(vq).scalars().all() + return run_sql(vq).scalars().all() def get_velp_label_content_for_document( @@ -272,7 +272,7 @@ def get_velp_label_content_for_document( """ vlcs = ( - db.session.execute( + run_sql( select(VelpLabelContent) .filter_by(language_id=language_id) .join(LabelInVelp, VelpLabelContent.velplabel_id == LabelInVelp.label_id) From 8f22f38828b41e1ee64eabf77b5bd5f2f89091e9 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Sat, 29 Jul 2023 16:53:27 +0300 Subject: [PATCH 13/34] Fix typos in models after refactor --- timApp/auth/oauth2/models.py | 4 ++-- timApp/messaging/{MIT-license.txt => LICENSE} | 0 timApp/migrations/README | 1 - timApp/velp/annotation_model.py | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) rename timApp/messaging/{MIT-license.txt => LICENSE} (100%) delete mode 100644 timApp/migrations/README diff --git a/timApp/auth/oauth2/models.py b/timApp/auth/oauth2/models.py index ae48d6bb2d..22f831737d 100644 --- a/timApp/auth/oauth2/models.py +++ b/timApp/auth/oauth2/models.py @@ -113,8 +113,8 @@ class OAuth2Token(DbModel, TokenMixin): __tablename__ = "oauth2_token" id: Mapped[int] = mapped_column(primary_key=True) - user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) - user: Mapped["User"] = relationship() + user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("useraccount.id")) + user: Mapped[Optional["User"]] = relationship() client_id: Mapped[Optional[str]] = mapped_column(String(48)) token_type: Mapped[Optional[str]] = mapped_column(String(40)) diff --git a/timApp/messaging/MIT-license.txt b/timApp/messaging/LICENSE similarity index 100% rename from timApp/messaging/MIT-license.txt rename to timApp/messaging/LICENSE diff --git a/timApp/migrations/README b/timApp/migrations/README deleted file mode 100644 index 98e4f9c44e..0000000000 --- a/timApp/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/timApp/velp/annotation_model.py b/timApp/velp/annotation_model.py index a2b9fd1447..20527f263c 100644 --- a/timApp/velp/annotation_model.py +++ b/timApp/velp/annotation_model.py @@ -94,7 +94,7 @@ class Annotation(DbModel): paragraph_id_start: Mapped[Optional[str]] """The id of the paragraph where this annotation starts from (in case this is a paragraph annotation).""" - paragraph_id_end: Mapped[Optional[int]] + paragraph_id_end: Mapped[Optional[str]] """The id of the paragraph where this annotation ends (in case this is a paragraph annotation).""" offset_start: Mapped[Optional[int]] From 3a0b04819eeafb9efd027fd188ff033bde35c53d Mon Sep 17 00:00:00 2001 From: dezhidki Date: Sat, 29 Jul 2023 21:41:57 +0300 Subject: [PATCH 14/34] Upgrade to PostgreSQL 15 --- cli/templates/docker/docker-compose.tmpl.yml | 12 ++++++++---- tim_common/timjsonencoder.py | 12 ++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cli/templates/docker/docker-compose.tmpl.yml b/cli/templates/docker/docker-compose.tmpl.yml index def0b66f8a..fe490a697f 100644 --- a/cli/templates/docker/docker-compose.tmpl.yml +++ b/cli/templates/docker/docker-compose.tmpl.yml @@ -71,9 +71,9 @@ services: profiles: - prod - dev - image: postgres:11 + image: postgres:15 volumes: - - data11:/var/lib/postgresql/data + - db_data:/var/lib/postgresql/data command: [ "postgres", "-c", "log_statement=mod", @@ -88,7 +88,9 @@ services: - /run/postgresql - /tmp environment: + POSTGRES_USER: postgres POSTGRES_PASSWORD: postgresql + POSTGRES_HOST_AUTH_METHOD: "md5" ports: ${ jsonify(["5432:5432"] if tim.is_dev else []) } logging: # According to docs, "local" is more compact and performant than the default json-file driver. @@ -97,7 +99,7 @@ services: max-size: "50m" max-file: "100" postgresql-test: - image: postgres:11 + image: postgres:15 profiles: - test - dev @@ -106,7 +108,9 @@ services: - /var/lib/postgresql/data ports: ${ jsonify(["5433:5432"] if tim.is_dev else []) } environment: + POSTGRES_USER: postgres POSTGRES_PASSWORD: postgresql + POSTGRES_HOST_AUTH_METHOD: "md5" csplugin: image: ${compose.images_repository}/cs3:${csplugin.target}-${csplugin.image_tag} build: @@ -427,7 +431,7 @@ ${ partial("docker", "csplugin_mongodb.tmpl.yml") if csplugin.is_mongodb_enabled read_only: true ${ partial("docker", "mailman.tmpl.yml") if mailman.is_dev else "" } volumes: - data11: + db_data: cache: caddy_data: caddy_config: diff --git a/tim_common/timjsonencoder.py b/tim_common/timjsonencoder.py index d95426bb51..36b6dbbaf1 100644 --- a/tim_common/timjsonencoder.py +++ b/tim_common/timjsonencoder.py @@ -27,6 +27,15 @@ def loads(self, s: str | bytes, **kwargs: Any) -> Any: return json.loads(s, **kwargs) +SQA_DBMODEL_ATTRS = { + "metadata", + "query", + "query_class", + "registry", + "type_annotation_map", +} + + class TimJsonEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, datetime.datetime): @@ -58,8 +67,7 @@ def default(self, o): flds = [ f for f in flds - if not f.startswith("_") - and f not in ["metadata", "query", "query_class", "registry"] + if not f.startswith("_") and f not in SQA_DBMODEL_ATTRS ] for field in flds: value = o.__getattribute__(field) From 3a0c714d3635d4d721d5c4f2ab31c974dee40897 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Tue, 1 Aug 2023 12:15:33 +0300 Subject: [PATCH 15/34] Fix translation regression --- timApp/document/translation/routes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/timApp/document/translation/routes.py b/timApp/document/translation/routes.py index 306af5e317..e73dea8f58 100644 --- a/timApp/document/translation/routes.py +++ b/timApp/document/translation/routes.py @@ -23,6 +23,7 @@ from flask import request, Blueprint from sqlalchemy import select, delete from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import with_polymorphic from timApp.auth.accesshelper import ( get_doc_or_abort, @@ -155,9 +156,9 @@ def get_languages(source_languages: bool) -> Response: # TODO Maybe change to use an id instead? tr = ( run_sql( - select(TranslationService) - .with_polymorphic("*") - .filter(TranslationService.service_name == translator) + select(with_polymorphic(TranslationService, "*")).filter( + TranslationService.service_name == translator + ) ) .scalars() .one() @@ -430,11 +431,10 @@ def get_translators() -> Response: translationservice_names = ( run_sql(select(TranslationService.service_name)).scalars().all() ) - # The SQLAlchemy query returns a list of tuples even when values of a - # single column were requested, so they must be unpacked. + # TODO Add "Manual" to the TranslationService-table instead of hardcoding # here (and elsewhere)? - sl = ["Manual"] + [x[0] for x in translationservice_names] + sl = ["Manual"] + [x for x in translationservice_names] return json_response(sl) From 42cbc3b4c9814eea0e3d335dc1a59b3941ec71b7 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Tue, 1 Aug 2023 12:40:38 +0300 Subject: [PATCH 16/34] Fix Celery not working --- cli/templates/docker/docker-compose.tmpl.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/templates/docker/docker-compose.tmpl.yml b/cli/templates/docker/docker-compose.tmpl.yml index fe490a697f..988babac2c 100644 --- a/cli/templates/docker/docker-compose.tmpl.yml +++ b/cli/templates/docker/docker-compose.tmpl.yml @@ -262,7 +262,7 @@ ${ partial("docker", "csplugin_mongodb.tmpl.yml") if csplugin.is_mongodb_enabled - redis_data:/data celery: image: ${compose.images_repository}/tim:${tim.image_tag} - command: celery worker -A timApp.tim_celery.celery --concurrency 4 + command: celery -A timApp.tim_celery.celery worker --concurrency 4 profiles: - prod - dev @@ -295,8 +295,8 @@ ${ partial("docker", "csplugin_mongodb.tmpl.yml") if csplugin.is_mongodb_enabled - prod - dev command: >- - celery beat - -A timApp.tim_celery.celery + celery -A timApp.tim_celery.celery + beat -S timApp.celery_sqlalchemy_scheduler.schedulers:DatabaseScheduler --pidfile /var/run/celery/celerybeat.pid volumes: From b018149ccf9d23772395a1859bd48f5caf144bc9 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Tue, 1 Aug 2023 14:07:00 +0300 Subject: [PATCH 17/34] Fix regression in user answer fetching --- timApp/util/get_fields.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/timApp/util/get_fields.py b/timApp/util/get_fields.py index 7b758779ff..7b588c5e5c 100644 --- a/timApp/util/get_fields.py +++ b/timApp/util/get_fields.py @@ -380,10 +380,11 @@ def get_fields_and_users( q = select(User).filter(User.id.in_(q)) else: q = q1 - q = q.with_only_columns(User).order_by(User.id).options(lazyload(User.groups)) + q = q.order_by(User.id).options(lazyload(User.groups)) if member_filter_type != MembershipFilter.Current: q = q.options(selectinload(User.memberships)) - users: list[User] = run_sql(q).scalars().all() + # Filter out duplicate rows because of the join + users: list[User] = run_sql(q).scalars().unique().all() user_map = {} for u in users: user_map[u.id] = u @@ -465,7 +466,7 @@ def get_fields_and_users( user = users[user_index] assert user.id == uid obj = {"user": user, "fields": user_tasks, "styles": user_fieldstyles} - res.append(obj) + res.append(UserFieldObj(**obj)) m_add = get_membership_added(user, group_id_set) m_end = ( get_membership_end(user, group_id_set) From e04d0b84c6d14b6428e521686f88d82d54008510 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Tue, 1 Aug 2023 14:47:46 +0300 Subject: [PATCH 18/34] Fix MyPy errors --- poetry.lock | 75 +------------------ pyproject.toml | 13 ++-- timApp/answer/answer.py | 8 +- timApp/answer/answers.py | 35 ++++++--- timApp/auth/access/routes.py | 3 +- timApp/auth/oauth2/models.py | 22 +++--- timApp/auth/oauth2/oauth2.py | 2 +- timApp/auth/saml/attributemaps/__init__.py | 0 timApp/auth/session/model.py | 8 +- timApp/auth/session/routes.py | 6 +- timApp/auth/session/util.py | 4 +- timApp/backup/__init__.py | 0 timApp/backup/backup_routes.py | 24 +++--- timApp/celery_sqlalchemy_scheduler/models.py | 2 +- timApp/document/course/__init__.py | 0 timApp/document/docentry.py | 8 +- timApp/document/macroinfo.py | 4 +- timApp/document/translation/deepl.py | 44 +++++++---- timApp/document/translation/language.py | 2 +- timApp/document/translation/translator.py | 15 ++-- timApp/messaging/messagelist/emaillist.py | 6 +- .../messagelist/messagelist_models.py | 35 +++++---- .../messagelist/messagelist_utils.py | 48 +++++++----- timApp/messaging/messagelist/routes.py | 17 +++-- timApp/messaging/timMessage/routes.py | 11 +-- timApp/note/routes.py | 4 +- timApp/peerreview/util/groups.py | 4 +- timApp/peerreview/util/peerreview_utils.py | 13 ++-- timApp/plugin/calendar/__init__.py | 0 timApp/plugin/calendar/calendar.py | 26 +++---- timApp/plugin/calendar/models.py | 10 +-- timApp/plugin/group_join/group_join.py | 13 ++-- timApp/plugin/importdata/__init__.py | 0 timApp/plugin/importdata/importData.py | 12 +-- timApp/plugin/jsrunner/util.py | 8 +- timApp/plugin/reviewcanvas/__init__.py | 0 timApp/plugin/tableform/__init__.py | 0 timApp/plugin/tableform/tableForm.py | 18 ++++- timApp/plugin/tape/__init__.py | 0 timApp/plugin/timmenu/__init__.py | 0 timApp/plugin/userselect/userselect.py | 6 +- timApp/scheduling/__init__.py | 0 timApp/scheduling/scheduling_routes.py | 27 +++++-- timApp/sisu/parse_display_name.py | 4 +- timApp/sisu/scim.py | 7 +- timApp/sisu/sisu.py | 49 ++++++------ .../manage-read-receipt.component.ts | 2 +- timApp/timdb/types.py | 17 +++-- timApp/user/settings/__init__.py | 0 timApp/user/settings/settings.py | 4 +- timApp/user/settings/styles.py | 5 +- timApp/user/usergroup.py | 6 ++ timApp/util/file_utils.py | 9 ++- timApp/util/flask/typedblueprint.py | 2 +- timApp/util/get_fields.py | 11 ++- timApp/velp/annotation.py | 20 ++++- timApp/velp/annotations.py | 7 +- timApp/velp/velp.py | 6 +- timApp/velp/velpgroups.py | 20 ++--- timApp/velp/velps.py | 8 +- 60 files changed, 390 insertions(+), 320 deletions(-) create mode 100644 timApp/auth/saml/attributemaps/__init__.py create mode 100644 timApp/backup/__init__.py create mode 100644 timApp/document/course/__init__.py create mode 100644 timApp/plugin/calendar/__init__.py create mode 100644 timApp/plugin/importdata/__init__.py create mode 100644 timApp/plugin/reviewcanvas/__init__.py create mode 100644 timApp/plugin/tableform/__init__.py create mode 100644 timApp/plugin/tape/__init__.py create mode 100644 timApp/plugin/timmenu/__init__.py create mode 100644 timApp/scheduling/__init__.py create mode 100644 timApp/user/settings/__init__.py diff --git a/poetry.lock b/poetry.lock index cb6b8ec794..2cf8575569 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1214,79 +1214,6 @@ monitor = ["psutil (>=5.7.0)"] recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"] -[[package]] -name = "greenlet" -version = "2.0.2" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -files = [ - {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, - {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, - {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, - {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, - {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, - {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, - {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, - {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, - {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, - {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, - {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, - {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, - {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, - {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, - {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, - {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, - {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, - {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, - {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, - {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, - {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, - {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, -] - -[package.extras] -docs = ["Sphinx", "docutils (<0.18)"] -test = ["objgraph", "psutil"] - [[package]] name = "greenlet" version = "3.0.0a1" @@ -3750,4 +3677,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "06cbdd0ef0eeb7af47bd78f37fa371eb599eb8f3cee07ddd073da50c883256b8" +content-hash = "a6065162074bdfebee2a8cb4feb62fdfd60d80c7c529122b770c8bf935158aa6" diff --git a/pyproject.toml b/pyproject.toml index 9a5182dd32..0aa0402cb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,7 +122,10 @@ exclude = [ 'timApp/tim_files', 'mailman/', 'tools/', - 'certs/' + 'certs/', + 'timApp/auth/saml/dev', + 'timApp/auth/saml/prod', + 'timApp/auth/saml/test', ] # Some modules have their own internal type checking, so we will always need to follow their imports @@ -134,11 +137,11 @@ module = [ "werkzeug.*", "bcrypt", "langcodes", - "cli" + "cli", + "sqlalchemy.*", ] follow_imports = "normal" -# Rewrite above comment using the TOML syntax [[tool.mypy.overrides]] module = [ # Ignore errors in tests @@ -169,10 +172,6 @@ module = [ "lxml.*", "saml2", "saml2.*", - "sqlalchemy", - "sqlalchemy.dialects", - "sqlalchemy.exc", - "sqlalchemy.orm", "webargs.flaskparser", "flask_wtf", "isodate", diff --git a/timApp/answer/answer.py b/timApp/answer/answer.py index 7b22abab7b..dfbe51c585 100644 --- a/timApp/answer/answer.py +++ b/timApp/answer/answer.py @@ -2,11 +2,11 @@ from typing import Any, Optional, List, TYPE_CHECKING from sqlalchemy import func, ForeignKey -from sqlalchemy.orm import mapped_column, Mapped, relationship +from sqlalchemy.orm import mapped_column, Mapped, relationship, DynamicMapped from timApp.answer.answer_models import UserAnswer, AnswerUpload from timApp.plugin.taskid import TaskId -from timApp.timdb.sqa import db, include_if_loaded +from timApp.timdb.sqa import include_if_loaded from timApp.timdb.types import datetime_tz, DbModel if TYPE_CHECKING: @@ -60,7 +60,7 @@ class Answer(DbModel): uploads: Mapped[List["AnswerUpload"]] = relationship( back_populates="answer", lazy="dynamic" ) - users: Mapped[List["User"]] = relationship( + users: DynamicMapped["User"] = relationship( secondary=UserAnswer.__table__, back_populates="answers", lazy="dynamic" ) users_all: Mapped[List["User"]] = relationship( @@ -78,7 +78,7 @@ def content_as_json(self) -> dict: return json.loads(self.content) def get_answer_number(self) -> int: - u: User = self.users.first() + u: User | None = self.users.first() if not u: return 1 return u.get_answers_for_task(self.task_id).filter(Answer.id <= self.id).count() diff --git a/timApp/answer/answers.py b/timApp/answer/answers.py index ca6170ba76..4e875c481a 100644 --- a/timApp/answer/answers.py +++ b/timApp/answer/answers.py @@ -6,7 +6,6 @@ from datetime import datetime, timedelta from enum import Enum from typing import ( - Iterable, Any, Generator, TypeVar, @@ -14,16 +13,28 @@ DefaultDict, TypedDict, ItemsView, + cast, + Sequence, ) # This ref exits in bs4 but doesn't seem to be correctly exported # noinspection PyUnresolvedReferences from bs4 import UnicodeDammit from flask import current_app -from sqlalchemy import func, Numeric, Float, true, case, select +from sqlalchemy import ( + func, + Numeric, + Float, + true, + case, + select, + Result, + Label, +) from sqlalchemy.dialects.postgresql import aggregate_order_by from sqlalchemy.orm import defaultload, selectinload, contains_eager from sqlalchemy.sql import Select, Subquery +from sqlalchemy.sql.elements import OperatorExpression from timApp.answer.answer import Answer from timApp.answer.answer_models import AnswerTag, UserAnswer @@ -59,7 +70,7 @@ def get_answers_query(task_id: TaskId, users: list[User], only_valid: bool) -> S .filter(User.id.in_([u.id for u in users])) .group_by(Answer.id) .having( - (func.array_agg(aggregate_order_by(User.id, User.id))) + (func.array_agg(aggregate_order_by(User.id, User.id))) # type: ignore[no-untyped-call] == sorted(u.id for u in users) ) .with_only_columns(Answer.id) @@ -298,6 +309,7 @@ def get_all_answers( if options.consent is not None: stmt = stmt.filter_by(consent=options.consent) + counts: Label[bool] | Label[int] match options.age: case AgeOptions.MIN: minmax = func.min(Answer.id).label("minmax") @@ -308,6 +320,8 @@ def get_all_answers( case AgeOptions.ALL: minmax = Answer.id.label("minmax") counts = Answer.valid.label("count") + case _: + raise ValueError(f"Invalid age option: {options.age}") # stmt = stmt.add_columns(minmax, counts) if options.age != AgeOptions.ALL: @@ -333,7 +347,7 @@ def get_all_answers( if options.print == AnswerPrintOptions.ANSWERS_NO_LINE: lf = "" - qq: Iterable[tuple[Answer, User, int]] = run_sql(stmt) + qq: Result[tuple[Answer, User, int]] = run_sql(stmt) cnt = 0 hidden_user_names: dict[str, str] = {} @@ -525,8 +539,10 @@ def valid_answers_query(task_ids: list[TaskId], valid: bool | None = True) -> Se return select(Answer).filter(valid_taskid_filter(task_ids, valid)) -def valid_taskid_filter(task_ids: list[TaskId], valid: bool | None = True) -> Any: - res = Answer.task_id.in_(task_ids_to_strlist(task_ids)) +def valid_taskid_filter( + task_ids: list[TaskId], valid: bool | None = True +) -> OperatorExpression[bool]: + res: OperatorExpression[bool] = Answer.task_id.in_(task_ids_to_strlist(task_ids)) if valid is not None: res = res & (Answer.valid == valid) return res @@ -588,7 +604,7 @@ def get_users_for_tasks( .with_only_columns( Answer.task_id, UserAnswer.user_id.label("uid"), - func.max(Answer.id).filter(Answer.valid == True).label("aid_valid"), + func.max(Answer.id).filter(Answer.valid == True).label("aid_valid"), # type: ignore[no-untyped-call] func.max(Answer.id).label("aid_any"), *time_labels, ) @@ -702,6 +718,7 @@ def get_users_for_tasks( def g() -> Generator[UserTaskEntry, None, None]: for r in run_sql(main_stmt): + # noinspection PyProtectedMember d = r._asdict() d["user"] = d.pop("User") task = d["task_points"] @@ -713,7 +730,7 @@ def g() -> Generator[UserTaskEntry, None, None]: else: tot = velp d["total_points"] = tot - yield d + yield cast(UserTaskEntry, d) result = list(g()) return result @@ -1098,7 +1115,7 @@ def add_missing_users_from_groups(result: list, usergroups: list[UserGroup]) -> return result -def get_global_answers(parsed_task_ids: dict[str, TaskId]) -> list[Answer]: +def get_global_answers(parsed_task_ids: dict[str, TaskId]) -> Sequence[Answer]: sq2 = ( select(Answer) .filter( diff --git a/timApp/auth/access/routes.py b/timApp/auth/access/routes.py index 66f1b32552..ce8e65c4fa 100644 --- a/timApp/auth/access/routes.py +++ b/timApp/auth/access/routes.py @@ -3,6 +3,7 @@ """ from dataclasses import field +from typing import Sequence from flask import Response from sqlalchemy import select @@ -71,7 +72,7 @@ def lock_active_groups(group_ids: list[int] | None) -> Response: } if not user.is_admin: - groups: list[UserGroup] = ( + groups: Sequence[UserGroup] = ( run_sql(select(UserGroup).filter(UserGroup.id.in_(group_ids_set))) .scalars() .all() diff --git a/timApp/auth/oauth2/models.py b/timApp/auth/oauth2/models.py index 22f831737d..84788b2095 100644 --- a/timApp/auth/oauth2/models.py +++ b/timApp/auth/oauth2/models.py @@ -126,19 +126,19 @@ class OAuth2Token(DbModel, TokenMixin): refresh_token_revoked_at: Mapped[int] = mapped_column(default=0) expires_in: Mapped[int] = mapped_column(default=0) - def check_client(self, client): + def check_client(self, client: ClientMixin) -> bool: return self.client_id == client.get_client_id() - def get_scope(self): + def get_scope(self) -> str | None: return self.scope - def get_expires_in(self): + def get_expires_in(self) -> int: return self.expires_in - def is_revoked(self): - return self.access_token_revoked_at or self.refresh_token_revoked_at + def is_revoked(self) -> bool: + return bool(self.access_token_revoked_at) or bool(self.refresh_token_revoked_at) - def is_expired(self): + def is_expired(self) -> bool: if not self.expires_in: return False @@ -164,17 +164,17 @@ class OAuth2AuthorizationCode(DbModel, AuthorizationCodeMixin): code_challenge: Mapped[Optional[str]] code_challenge_method: Mapped[Optional[str]] = mapped_column(String(48)) - def is_expired(self): + def is_expired(self) -> bool: return self.auth_time + 300 < time.time() - def get_redirect_uri(self): + def get_redirect_uri(self) -> str | None: return self.redirect_uri - def get_scope(self): + def get_scope(self) -> str | None: return self.scope - def get_auth_time(self): + def get_auth_time(self) -> int: return self.auth_time - def get_nonce(self): + def get_nonce(self) -> str | None: return self.nonce diff --git a/timApp/auth/oauth2/oauth2.py b/timApp/auth/oauth2/oauth2.py index d247c9fbf0..78420db211 100644 --- a/timApp/auth/oauth2/oauth2.py +++ b/timApp/auth/oauth2/oauth2.py @@ -26,7 +26,7 @@ class RefreshTokenGrant(grants.RefreshTokenGrant): INCLUDE_NEW_REFRESH_TOKEN = True def authenticate_refresh_token(self, refresh_token: str) -> OAuth2Token | None: - token: OAuth2Token = ( + token: OAuth2Token | None = ( run_sql(select(OAuth2Token).filter_by(refresh_token=refresh_token).limit(1)) .scalars() .first() diff --git a/timApp/auth/saml/attributemaps/__init__.py b/timApp/auth/saml/attributemaps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/auth/session/model.py b/timApp/auth/session/model.py index 70b9eb0ad4..ec0f9a6e8a 100755 --- a/timApp/auth/session/model.py +++ b/timApp/auth/session/model.py @@ -56,17 +56,17 @@ class UserSession(DbModel): User that owns the session. Relationship to :attr:`user_id`. """ - def _get_expired(self) -> bool: + @hybrid_property + def expired(self) -> bool: """ + Whether the user session is expired. + :return: Whether the user session is expired. """ # == is needed because this is a hybrid property # noinspection PyComparisonWithNone return self.expired_at != None # noqa: E712 - expired = hybrid_property(_get_expired) - """Whether the user session is expired.""" - def expire(self) -> None: """ Expires the current user session. diff --git a/timApp/auth/session/routes.py b/timApp/auth/session/routes.py index d9c42dfbb4..98f2df9d6c 100644 --- a/timApp/auth/session/routes.py +++ b/timApp/auth/session/routes.py @@ -5,7 +5,7 @@ from _csv import QUOTE_ALL from dataclasses import field from enum import Enum -from typing import Any +from typing import Any, Sequence from flask import Response from sqlalchemy import update, select @@ -203,12 +203,12 @@ def validate_all() -> Response: """ verify_admin() - all_usersnames: list[tuple[str]] = ( + all_usersnames: Sequence[str] = ( run_sql(select(User.name).join(UserSession).distinct(UserSession.user_id)) .scalars() .all() ) - for (user,) in all_usersnames: + for user in all_usersnames: verify_session_for(user) db.session.commit() diff --git a/timApp/auth/session/util.py b/timApp/auth/session/util.py index 77c0c0fec4..f70555433a 100644 --- a/timApp/auth/session/util.py +++ b/timApp/auth/session/util.py @@ -29,7 +29,7 @@ def _max_concurrent_sessions() -> int | None: def _get_active_session_count(user: User) -> int: return db.session.scalar( select(func.count(UserSession.session_id)).filter( - (UserSession.user == user) & ~UserSession.expired + (UserSession.user == user) & ~UserSession.expired # type: ignore ) ) @@ -160,7 +160,7 @@ def has_valid_session(user: User | None = None) -> bool: run_sql( select(UserSession.session_id) .filter( - (UserSession.user == user) + (UserSession.user_id == user.id) & (UserSession.session_id == session_id) & ~UserSession.expired ) diff --git a/timApp/backup/__init__.py b/timApp/backup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/backup/backup_routes.py b/timApp/backup/backup_routes.py index 3b7f3dbcaf..a952a9ef67 100644 --- a/timApp/backup/backup_routes.py +++ b/timApp/backup/backup_routes.py @@ -1,3 +1,5 @@ +from typing import Sequence + from flask import Response from sqlalchemy import select @@ -50,16 +52,20 @@ def receive_user_memberships( user.add_to_group(ug, None) if removed_memberships: - removed_memberships_objs: list[UserGroupMember] = run_sql( - select(UserGroupMember) - .join(UserGroup, UserGroupMember.group) - .join(User, UserGroupMember.user) - .filter( - (User.name == user.name) - & UserGroup.name.in_(removed_memberships) - & membership_current + removed_memberships_objs: Sequence[UserGroupMember] = ( + run_sql( + select(UserGroupMember) + .join(UserGroup, UserGroupMember.group) + .join(User, UserGroupMember.user) + .filter( + (User.name == user.name) + & UserGroup.name.in_(removed_memberships) + & membership_current + ) ) - ).scalars() + .scalars() + .all() + ) for ugm in removed_memberships_objs: ugm.set_expired() diff --git a/timApp/celery_sqlalchemy_scheduler/models.py b/timApp/celery_sqlalchemy_scheduler/models.py index aba5ffb049..b1cdafdaa4 100644 --- a/timApp/celery_sqlalchemy_scheduler/models.py +++ b/timApp/celery_sqlalchemy_scheduler/models.py @@ -232,7 +232,7 @@ class PeriodicTask(ModelBase, ModelMixin): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) block_id: Mapped[Optional[int]] = mapped_column(sa.ForeignKey("block.id")) - block: Mapped[Optional[Block]] = relationship(Block) + block: Mapped[Optional[Block]] = relationship() # name name: Mapped[Optional[str]] = mapped_column(sa.String(255), unique=True) # task name diff --git a/timApp/document/course/__init__.py b/timApp/document/course/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/document/docentry.py b/timApp/document/docentry.py index 377522f236..d970a2dad6 100644 --- a/timApp/document/docentry.py +++ b/timApp/document/docentry.py @@ -35,7 +35,7 @@ class DocEntry(DbModel, DocInfo): TODO: Improve the name. """ - id: Mapped[int] = mapped_column(ForeignKey("block.id")) + id: Mapped[int] = mapped_column(ForeignKey("block.id")) # type: ignore """Document identifier.""" public: Mapped[bool] = mapped_column(default=True) @@ -93,11 +93,11 @@ def translations(self) -> list[Translation]: @staticmethod def get_all() -> list[DocEntry]: - return run_sql(select(DocEntry)).scalars().all() + return run_sql(select(DocEntry)).scalars().all() # type: ignore @staticmethod def find_all_by_id(doc_id: int) -> list[DocEntry]: - return run_sql(select(DocEntry).filter_by(id=doc_id)).scalars().all() + return run_sql(select(DocEntry).filter_by(id=doc_id)).scalars().all() # type: ignore @staticmethod def find_by_id(doc_id: int, docentry_load_opts: Any = None) -> DocInfo | None: @@ -270,7 +270,7 @@ def get_documents( stmt = stmt.options(query_options) result = run_sql(stmt).scalars().all() if not filter_user: - return result + return result # type: ignore return [r for r in result if filter_user.has_view_access(r)] diff --git a/timApp/document/macroinfo.py b/timApp/document/macroinfo.py index b46b1c6494..0ca809f3b8 100644 --- a/timApp/document/macroinfo.py +++ b/timApp/document/macroinfo.py @@ -15,7 +15,7 @@ create_environment, TimSandboxedEnvironment, ) -from timApp.util.rndutils import get_rands_as_dict +from timApp.util.rndutils import get_rands_as_dict, SeedType from timApp.util.utils import cached_property if TYPE_CHECKING: @@ -154,7 +154,7 @@ def get_user_specific_macros(user_ctx: UserContext) -> dict[str, str | None]: def get_rnd_macros( rndmacros_setting: dict[str, str], user: User | None ) -> dict[str, str]: - rnd_seed = user.name if user else None + rnd_seed: SeedType | None = user.name if user else None state = None ret = {} rndm = rndmacros_setting diff --git a/timApp/document/translation/deepl.py b/timApp/document/translation/deepl.py index a0bcb96c3a..3b73824493 100644 --- a/timApp/document/translation/deepl.py +++ b/timApp/document/translation/deepl.py @@ -55,26 +55,18 @@ def __init__(self, values: dict): self.ignore_tag = values["ignore_tag"] self.service_url = values["service_url"] - # TODO Would be better as non-optional, but that prevents creating + # TODO: Would be better as non-optional, but that prevents creating # non-DeeplTranslationService -subclasses of TranslationService. service_url: Mapped[Optional[str]] """The url base for the API calls.""" - # TODO Would be better as non-optional, but that prevents creating + # TODO: Would be better as non-optional, but that prevents creating # non-DeeplTranslationService -subclasses of TranslationService. ignore_tag: Mapped[Optional[str]] """The XML-tag name to use for ignoring pieces of text when XML-handling is used. Should be chosen to be some uncommon string not found in many texts. """ - # headers: ClassVar[dict[str, str]] - # """Request-headers needed for authentication with the API-key.""" - # - # source_Language_code: ClassVar[str] - # """The source language's code (helps handling regional variants that DeepL - # doesn't differentiate). - # """ - def register(self, user_group: UserGroup) -> None: """ Set headers to use the user group's API-key ready for translation @@ -106,6 +98,30 @@ def register(self, user_group: UserGroup) -> None: ) self.headers = {"Authorization": f"DeepL-Auth-Key {api_key[0].api_key}"} + @property + def headers(self) -> dict[str, str]: + """Request-headers needed for authentication with the API-key.""" + return getattr(self, "_headers") if hasattr(self, "_headers") else {} + + @headers.setter + def headers(self, value: dict[str, str]) -> None: + setattr(self, "_headers", value) + + @property + def source_Language_code(self) -> str: + """The source language's code (helps handling regional variants that DeepL + doesn't differentiate). + """ + return ( + getattr(self, "_source_Language_code") + if hasattr(self, "_source_Language_code") + else None + ) + + @source_Language_code.setter + def source_Language_code(self, value: str) -> None: + setattr(self, "_source_Language_code", value) + # TODO Change the dicts to DeepLTranslateParams and DeeplResponse or smth def _post(self, url_slug: str, data: dict | None = None) -> dict: """ @@ -116,7 +132,7 @@ def _post(self, url_slug: str, data: dict | None = None) -> dict: :param data: Data to be transmitted along the request. :return: The JSON-response returned by the API. """ - resp = post(self.service_url + "/" + url_slug, data=data, headers=self.headers) + resp = post(f"{self.service_url}/{url_slug}", data=data, headers=self.headers) return self._handle_post_response(resp) @@ -250,7 +266,7 @@ def _translate( } return session.post( - self.service_url + "/translate", data=data, headers=self.headers + f"{self.service_url}/translate", data=data, headers=self.headers ) @cache.memoize(timeout=LANGUAGES_CACHE_TIMEOUT, args_to_ignore=["self"]) @@ -339,7 +355,7 @@ def translate( session, protected_texts[i : i + 50], # Send uppercase, because it is used in DeepL documentation. - source_lang_code.upper(), + source_lang_code.upper() if source_lang_code else None, target_lang.lang_code.upper(), # "1" (for example) keeps original document's empty newlines. split_sentences="1", @@ -347,7 +363,7 @@ def translate( # though DeepL should not make guesses of the content. preserve_formatting="0", tag_handling=tag_handling, - ignore_tags=[self.ignore_tag], + ignore_tags=[self.ignore_tag] if self.ignore_tag else [], ) translate_calls.append(call) diff --git a/timApp/document/translation/language.py b/timApp/document/translation/language.py index 6134d129d4..c016201452 100644 --- a/timApp/document/translation/language.py +++ b/timApp/document/translation/language.py @@ -100,7 +100,7 @@ def query_all(cls) -> list["Language"]: :return: All the languages found from database. """ - return run_sql(select(cls)).scalars().all() + return run_sql(select(cls)).scalars().all() # type: ignore def __str__(self) -> str: """ diff --git a/timApp/document/translation/translator.py b/timApp/document/translation/translator.py index b24c02a549..ff40bfbf12 100644 --- a/timApp/document/translation/translator.py +++ b/timApp/document/translation/translator.py @@ -19,6 +19,7 @@ __date__ = "25.4.2022" from dataclasses import dataclass +from typing import Optional import pypandoc from sqlalchemy import select, ForeignKey @@ -194,7 +195,7 @@ class TranslationServiceKey(DbModel): @staticmethod def get_by_user_group( user_group: UserGroup | None, - ) -> "TranslationServiceKey": + ) -> Optional["TranslationServiceKey"]: """ Query a key based on a group that could have access to it. @@ -202,11 +203,15 @@ def get_by_user_group( :return: The first matching TranslationServiceKey instance, if one is found. """ - return run_sql( - select(TranslationServiceKey).filter( - TranslationServiceKey.group_id == user_group + return ( + run_sql( + select(TranslationServiceKey) + .filter(TranslationServiceKey.group_id == user_group) + .limit(1) ) - ).first() + .scalars() + .first() + ) def to_json(self) -> dict: """ diff --git a/timApp/messaging/messagelist/emaillist.py b/timApp/messaging/messagelist/emaillist.py index f1a58bcf2c..2f53587871 100644 --- a/timApp/messaging/messagelist/emaillist.py +++ b/timApp/messaging/messagelist/emaillist.py @@ -286,7 +286,7 @@ def create_new_email_list(list_options: ListInfo, owner: User) -> None: raise -def get_list_ui_link(listname: str, domain: str | None) -> str | None: +def get_list_ui_link(listname: str | None, domain: str | None) -> str | None: """Get a link for a list to use for advanced email list options and moderation. The function assumes that Mailman uses Postorius as its web-UI. There exists no guarantee that other web-UIs would @@ -298,6 +298,8 @@ def get_list_ui_link(listname: str, domain: str | None) -> str | None: then return None. Return None if no connection to Mailman is configured. """ try: + if listname is None: + return None if domain is None or not config.MAILMAN_UI_LINK_PREFIX: return None if _client is None: @@ -729,7 +731,7 @@ def unfreeze_list(mlist: MailingList, msg_list: MessageListModel) -> None: try: mail_list_settings = mlist.settings mail_list_settings["default_member_action"] = "accept" - set_email_list_allow_nonmember(mlist, msg_list.non_member_message_pass) + set_email_list_allow_nonmember(mlist, msg_list.non_member_message_pass or False) mail_list_settings.save() except HTTPError as e: log_mailman(e, "In unfreeze_list()") diff --git a/timApp/messaging/messagelist/messagelist_models.py b/timApp/messaging/messagelist/messagelist_models.py index facc4ca5f2..5e92605687 100644 --- a/timApp/messaging/messagelist/messagelist_models.py +++ b/timApp/messaging/messagelist/messagelist_models.py @@ -43,10 +43,10 @@ class MessageListModel(DbModel): id: Mapped[int] = mapped_column(primary_key=True) - manage_doc_id: Mapped[Optional[int]] = mapped_column(ForeignKey("block.id")) + manage_doc_id: Mapped[int] = mapped_column(ForeignKey("block.id")) """The document which manages a message list.""" - name: Mapped[Optional[str]] + name: Mapped[str] """The name of a message list.""" can_unsubscribe: Mapped[Optional[bool]] @@ -179,12 +179,12 @@ def name_exists(name_candidate: str) -> bool: ) @property - def archive_policy(self) -> ArchiveType: + def archive_policy(self) -> ArchiveType | None: return self.archive @hybrid_property def mailman_list_id(self) -> str: - return self.name + "." + self.email_list_domain + return f"{self.name}.{self.email_list_domain}" def get_individual_members(self) -> list["MessageListMember"]: """Get all the members that are not groups. @@ -199,13 +199,13 @@ def get_individual_members(self) -> list["MessageListMember"]: return individuals def get_tim_members(self) -> list["MessageListTimMember"]: - """Get all members that have belong to a user group, i.e. TIM users and user groups. + """Get all members that belong to a user group, i.e. TIM users and user groups. :return: A list of MessageListTimMember objects. """ tim_members = [] for member in self.members: - if member.is_tim_member(): + if member.tim_member is not None: tim_members.append(member.tim_member) return tim_members @@ -234,7 +234,8 @@ def find_member( @property def email_address(self) -> str | None: """Full email address of the messagelist, if the list has been assigned an active address. - Otherwise None.""" + Otherwise None. + """ return ( f"{self.name}@{self.email_list_domain}" if self.email_list_domain else None ) @@ -243,10 +244,10 @@ def to_info(self) -> ListInfo: from timApp.messaging.messagelist.emaillist import get_list_ui_link return ListInfo( - name=self.name, + name=self.name or "unnamed_list", notify_owners_on_list_change=self.notify_owner_on_change, domain=self.email_list_domain, - archive=self.archive, + archive=self.archive or ArchiveType.NONE, default_reply_type=self.default_reply_type, verification_type=self.message_verification, tim_users_can_join=self.tim_user_can_join, @@ -334,14 +335,16 @@ def is_tim_member(self) -> bool: def is_personal_user(self) -> bool: """If this member is an individual user, i.e. a personal user group or an external member.""" - try: - gid = self.tim_member.group_id - except AttributeError: - # External members don't have a group_id attribute. + if self.tim_member is None: return self.is_external_member() - from timApp.user.usergroup import UserGroup - ug = db.session.exectute(select(UserGroup).filter_by(id=gid)).scalars().one() + ug = ( + db.session.exectute( + select(UserGroup).filter_by(id=self.tim_member.group_id) + ) + .scalars() + .one() + ) return ug.is_personal_group def is_group(self) -> bool: @@ -492,7 +495,7 @@ def get_email(self) -> str: :return: The email address. """ - return self.email_address + return self.email_address or f"member_{self.id}@noreply" def get_username(self) -> str: """External member's don't have usernames, but this is for consistency when using other methods.""" diff --git a/timApp/messaging/messagelist/messagelist_utils.py b/timApp/messaging/messagelist/messagelist_utils.py index 7deed577d7..d7cd4733ba 100644 --- a/timApp/messaging/messagelist/messagelist_utils.py +++ b/timApp/messaging/messagelist/messagelist_utils.py @@ -464,6 +464,8 @@ def check_archives_folder_exists(message_list: MessageListModel) -> Folder | Non """ if message_list.archive_policy is ArchiveType.NONE: return None + if not message_list.name: + return None archive_folder_path = f"{MESSAGE_LIST_ARCHIVE_FOLDER_PREFIX}/{remove_path_special_chars(message_list.name)}" archive_folder = Folder.find_by_path(archive_folder_path) if archive_folder is None: @@ -625,6 +627,8 @@ def parse_mailman_message(original: dict, msg_list: MessageListModel) -> BaseMes ) ) + assert msg_list.name is not None and msg_list.email_list_domain is not None + message = BaseMessage( message_list_name=msg_list.name, domain=msg_list.email_list_domain, @@ -741,8 +745,8 @@ def new_list(list_options: ListInfo, owner: User) -> tuple[DocInfo, MessageListM :return: The message list db model. """ msg_list = MessageListModel(name=list_options.name, archive=list_options.archive) - db.session.add(msg_list) doc_info = create_management_doc(msg_list, list_options, owner) + db.session.add(msg_list) check_archives_folder_exists(msg_list) return doc_info, msg_list @@ -766,7 +770,7 @@ def set_message_list_notify_owner_on_change( message_list.notify_owner_on_change = notify_owners_on_list_change_flag - if message_list.email_list_domain: + if message_list.email_list_domain and message_list.name: # Email lists have their own flag for notifying list owners for list changes. email_list = get_email_list_by_name( message_list.name, message_list.email_list_domain @@ -1022,7 +1026,9 @@ def add_new_message_list_group( the email list. """ # Check right to a group. Right checking is not required for personal groups, only user groups. - if not ug.is_personal_group and not has_manage_access(ug.admin_doc): + if not ug.is_personal_group and ( + not ug.admin_doc or not has_manage_access(ug.admin_doc) + ): return # Check for membership duplicates. @@ -1110,7 +1116,9 @@ def sync_message_list_on_add(user: User, new_group: UserGroup) -> None: # locally. # Get all the message lists for the user group. - for group_tim_member in new_group.messagelist_membership: + for ( + group_tim_member + ) in new_group.messagelist_membership: # type: MessageListTimMember group_message_list: MessageListModel = group_tim_member.message_list # Propagate the adding on message list's message channels. if group_message_list.email_list_domain: @@ -1123,8 +1131,8 @@ def sync_message_list_on_add(user: User, new_group: UserGroup) -> None: user.email, True, user.real_name, - group_tim_member.member.send_right, - group_tim_member.member.delivery_right, + group_tim_member.member.send_right or True, + group_tim_member.member.delivery_right or False, ) @@ -1174,7 +1182,7 @@ def set_message_list_member_removed_status( member.remove(removed) # Remove members from email list or return them there. if email_list: - if member.is_group(): + if member.tim_member and member.is_group(): ug = member.tim_member.user_group ug_members = ug.users for ug_member in ug_members: @@ -1183,10 +1191,14 @@ def set_message_list_member_removed_status( remove_email_list_membership(mlist_member) else: # Re-set the member's send and delivery rights on the email list. - set_email_list_member_send_status(mlist_member, member.send_right) - set_email_list_member_delivery_status( - mlist_member, member.delivery_right - ) + if member.send_right: + set_email_list_member_send_status( + mlist_member, member.send_right + ) + if member.delivery_right: + set_email_list_member_delivery_status( + mlist_member, member.delivery_right + ) elif member.is_personal_user(): # Make changes to member's status on the email list. mlist_member = get_email_list_member(email_list, member.get_email()) @@ -1195,10 +1207,12 @@ def set_message_list_member_removed_status( remove_email_list_membership(mlist_member) else: # Re-set the member's send and delivery rights on the email list. - set_email_list_member_send_status(mlist_member, member.send_right) - set_email_list_member_delivery_status( - mlist_member, member.delivery_right - ) + if member.send_right: + set_email_list_member_send_status(mlist_member, member.send_right) + if member.delivery_right: + set_email_list_member_delivery_status( + mlist_member, member.delivery_right + ) def set_member_send_delivery( @@ -1223,7 +1237,7 @@ def set_member_send_delivery( if member.is_personal_user(): mlist_member = get_email_list_member(email_list, member.get_email()) set_email_list_member_send_status(mlist_member, send) - elif member.is_group(): + elif member.tim_member and member.is_group(): # For group, set the delivery status for its members on the email list. ug = member.tim_member.user_group ug_members = ug.users # ug.current_memberships @@ -1242,7 +1256,7 @@ def set_member_send_delivery( if member.is_personal_user(): mlist_member = get_email_list_member(email_list, member.get_email()) set_email_list_member_delivery_status(mlist_member, delivery) - elif member.is_group(): + elif member.tim_member and member.is_group(): # For group, set the delivery status for its members on the email list. ug = member.tim_member.user_group ug_members = ug.users # ug.current_memberships diff --git a/timApp/messaging/messagelist/routes.py b/timApp/messaging/messagelist/routes.py index 4dd37801ae..5b9273acfb 100644 --- a/timApp/messaging/messagelist/routes.py +++ b/timApp/messaging/messagelist/routes.py @@ -32,7 +32,10 @@ GroupAndMembers, ArchiveType, ) -from timApp.messaging.messagelist.messagelist_models import MessageListModel +from timApp.messaging.messagelist.messagelist_models import ( + MessageListModel, + MessageListTimMember, +) from timApp.messaging.messagelist.messagelist_utils import ( verify_messagelist_name_requirements, new_list, @@ -435,7 +438,9 @@ def get_group_members(list_name: str) -> Response: ) # Get group. - groups = [member for member in message_list.members if member.is_group()] + groups: list[MessageListTimMember] = [ + member for member in message_list.members if member.is_group() + ] # At this point we assume we have a user that is a TIM user group. groups_and_members = [] @@ -445,11 +450,13 @@ def get_group_members(list_name: str) -> Response: # group, removed is None. group_members = [ MemberInfo( - name=user.real_name if not hide_names else f"User {user.id}", + name=user.real_name + if not hide_names and user.real_name + else f"User {user.id}", username=user.name if not hide_names else f"user{user.id}", email=user.email if not hide_names else "user@noreply", - sendRight=group.send_right, - deliveryRight=group.delivery_right, + sendRight=group.send_right or False, + deliveryRight=group.delivery_right or False, removed=None, ) for user in user_group.users diff --git a/timApp/messaging/timMessage/routes.py b/timApp/messaging/timMessage/routes.py index 07d9f58cf1..047469ab20 100644 --- a/timApp/messaging/timMessage/routes.py +++ b/timApp/messaging/timMessage/routes.py @@ -2,10 +2,11 @@ from dataclasses import dataclass, field from datetime import datetime from enum import Enum +from typing import Any, Sequence from flask import Response from isodate import datetime_isoformat -from sqlalchemy import tuple_, select +from sqlalchemy import tuple_, select, false from sqlalchemy.orm import contains_eager from timApp.auth.accesshelper import ( @@ -111,7 +112,7 @@ class TimMessageData: class TimMessageReadReceipt: message_id: int user_id: int - marked_as_read_on: datetime + marked_as_read_on: datetime | None can_mark_as_read: bool @@ -174,7 +175,7 @@ def get_tim_messages_as_list(item_id: int | None = None) -> list[TimMessageData] is_global = (InternalMessageDisplay.usergroup_id == None) & ( InternalMessageDisplay.display_doc_id == None ) - is_user_specific = False + is_user_specific: Any = false() can_see = (InternalMessageReadReceipt.marked_as_read_on == None) & ( (InternalMessage.expires == None) | (InternalMessage.expires > now) ) @@ -215,7 +216,7 @@ def get_tim_messages_as_list(item_id: int | None = None) -> list[TimMessageData] .filter((is_global | is_user_specific) & can_see) ) - messages: list[InternalMessage] = run_sql(stmt).scalars().all() + messages: Sequence[InternalMessage] = run_sql(stmt).scalars().all() full_messages = [] for message in messages: @@ -668,7 +669,7 @@ def get_recipient_users(recipients: list[str] | None) -> list[UserGroup]: select(UserGroup) .join(MessageListTimMember) .filter( - (MessageListTimMember.message_list == msg_list) + (MessageListTimMember.message_list_id == msg_list.id) & (MessageListTimMember.membership_ended == None) ) ) diff --git a/timApp/note/routes.py b/timApp/note/routes.py index 745d1e5a45..1b138f235a 100644 --- a/timApp/note/routes.py +++ b/timApp/note/routes.py @@ -136,13 +136,13 @@ def get_notes( access_restriction = UserNote.access == "everyone" if private: access_restriction = true() - time_restriction = true() + time_restriction: Any = true() if start: time_restriction = time_restriction & (UserNote.created >= start) if end: time_restriction = time_restriction & (UserNote.created < end) d_ids = [d.id for d in docs] - ns = ( + ns = list( run_sql( select(UserNote) .filter(UserNote.doc_id.in_(d_ids) & access_restriction & time_restriction) diff --git a/timApp/peerreview/util/groups.py b/timApp/peerreview/util/groups.py index 5a76152fc9..68c59819c7 100644 --- a/timApp/peerreview/util/groups.py +++ b/timApp/peerreview/util/groups.py @@ -1,7 +1,7 @@ from collections import defaultdict from datetime import datetime from random import shuffle -from typing import DefaultDict +from typing import DefaultDict, Sequence from sqlalchemy import select @@ -82,7 +82,7 @@ def generate_review_groups(doc: DocInfo, task_ids: list[TaskId]) -> None: # PeerReview rows and pairings will be the same for every task, even if target did not answer to some of tasks # If target has an answer in a task, try to add it to PeerReview table. If not, just leave it empty for t in task_ids: - answers: list[Answer] = ( + answers: Sequence[Answer] = ( run_sql(get_latest_answers_query(t, users, valid_only)).scalars().all() ) excluded_users: list[User] = [] diff --git a/timApp/peerreview/util/peerreview_utils.py b/timApp/peerreview/util/peerreview_utils.py index ff1fe452a4..314a22072b 100644 --- a/timApp/peerreview/util/peerreview_utils.py +++ b/timApp/peerreview/util/peerreview_utils.py @@ -25,7 +25,7 @@ def get_reviews_where_user_is_reviewer(d: DocInfo, user: User) -> list[PeerRevie stmt = get_reviews_where_user_is_reviewer_query(d, user).options( selectinload(PeerReview.reviewable) ) - return run_sql(stmt).scalars().all() + return run_sql(stmt).scalars().all() # type: ignore def get_reviews_where_user_is_reviewer_query(d: DocInfo, user: User) -> Select: @@ -33,7 +33,7 @@ def get_reviews_where_user_is_reviewer_query(d: DocInfo, user: User) -> Select: def get_all_reviews(doc: DocInfo) -> list[PeerReview]: - return run_sql(select(PeerReview).filter_by(block_id=doc.id)).scalars().all() + return run_sql(select(PeerReview).filter_by(block_id=doc.id)).scalars().all() # type: ignore def get_reviews_targeting_user(d: DocInfo, user: User) -> list[PeerReview]: @@ -41,7 +41,7 @@ def get_reviews_targeting_user(d: DocInfo, user: User) -> list[PeerReview]: stmt = get_reviews_targeting_user_query(d, user).options( selectinload(PeerReview.reviewable) ) - return run_sql(stmt).scalars().all() + return run_sql(stmt).scalars().all() # type: ignore def get_reviews_targeting_user_query(d: DocInfo, user: User) -> Select: @@ -56,7 +56,7 @@ def get_reviews_related_to_user(d: DocInfo, user: User) -> list[PeerReview]: (PeerReview.reviewable_id == user.id) | (PeerReview.reviewer_id == user.id) ) ) - return run_sql(stmt).scalars().all() + return run_sql(stmt).scalars().all() # type: ignore def has_review_access( @@ -111,7 +111,7 @@ def get_reviews_for_document(doc: DocInfo) -> list[PeerReview]: ) ) .scalars() - .all() + .all() # type: ignore ) @@ -145,7 +145,8 @@ def change_peerreviewers_for_user( .scalars() .first() ) - updated_user.reviewer_id = new_reviewers[i] + if updated_user: + updated_user.reviewer_id = new_reviewers[i] try: db.session.flush() except IntegrityError: diff --git a/timApp/plugin/calendar/__init__.py b/timApp/plugin/calendar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/plugin/calendar/calendar.py b/timApp/plugin/calendar/calendar.py index 5b62e89683..c55c10ce57 100644 --- a/timApp/plugin/calendar/calendar.py +++ b/timApp/plugin/calendar/calendar.py @@ -20,7 +20,7 @@ from enum import Enum from io import StringIO from textwrap import wrap -from typing import Literal +from typing import Literal, Any from flask import Response, render_template_string, url_for from marshmallow import missing @@ -364,7 +364,7 @@ def export_ical(user: User) -> Response: :param user: User to generate ICS link for :return: """ - user_data: ExportedCalendar = ( + user_data: ExportedCalendar | None = ( run_sql(select(ExportedCalendar).filter(ExportedCalendar.user_id == user.id)) .scalars() .one_or_none() @@ -411,7 +411,7 @@ def get_ical(opts: ICalFilterOptions) -> Response: :return: ICS file that can be exported otherwise 404 if user data does not exist. """ - user_data: ExportedCalendar = ( + user_data: ExportedCalendar | None = ( run_sql(select(ExportedCalendar).filter_by(calendar_hash=opts.key)) .scalars() .one_or_none() @@ -478,7 +478,7 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev stmt = select(Event.event_id) event_queries = [] - event_filter = false() + event_filter: Any = false() # Events come from different places: # 1. Events that are created by the user @@ -537,7 +537,7 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev if filter_opts.showBookedByMin is not None: booked_min_subquery = ( select(Event) - .filter(Event.creator == u) + .filter(Event.creator_user_id == u.id) .outerjoin(Enrollment) .group_by(Event.event_id) .with_only_columns( @@ -552,7 +552,7 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev ) event_queries.append(booked_min_query) - timing_filter = true() + timing_filter: Any = true() # Apply date filter to all events if filter_opts.fromDate: timing_filter &= Event.start_time >= filter_opts.fromDate @@ -560,13 +560,13 @@ def events_of_user(u: User, filter_opts: FilterOptions | None = None) -> list[Ev timing_filter &= Event.end_time <= filter_opts.toDate if event_queries: - stmt = stmt.union(*event_queries) - stmt = select(Event).filter(Event.event_id.in_(stmt)) + tmp = stmt.union(*event_queries) + main_stmt = select(Event).filter(Event.event_id.in_(tmp)) else: - stmt = stmt.with_only_columns(Event) - stmt = stmt.filter(timing_filter) + main_stmt = stmt.with_only_columns(Event) + main_stmt = main_stmt.filter(timing_filter) - return run_sql(stmt).scalars().all() + return run_sql(main_stmt).scalars().all() # type: ignore @calendar_plugin.get("/events", model=FilterOptions) @@ -811,8 +811,8 @@ def update_event(cal_event: CalendarEvent, event: Event) -> Event: if calendar_event.id is not missing: if not modify_existing: raise AccessDenied("Cannot modify existing events via this route") - event: Event = ( - run_sql(select(Event).filter_by(event_id=calendar_event.id)) + event: Event | None = ( + run_sql(select(Event).filter_by(event_id=calendar_event.id).limit(1)) .scalars() .first() ) diff --git a/timApp/plugin/calendar/models.py b/timApp/plugin/calendar/models.py index 583209c830..d665c419db 100644 --- a/timApp/plugin/calendar/models.py +++ b/timApp/plugin/calendar/models.py @@ -177,10 +177,10 @@ class Event(DbModel): event_id: Mapped[int] = mapped_column(primary_key=True) """Identification number of the event""" - location: Mapped[Optional[str]] + location: Mapped[str] = mapped_column(default="") """Location of the event""" - max_size: Mapped[Optional[int]] + max_size: Mapped[int] """How many people can attend the event""" start_time: Mapped[datetime_tz] @@ -189,13 +189,13 @@ class Event(DbModel): end_time: Mapped[datetime_tz] """End time of the event""" - message: Mapped[Optional[str]] + message: Mapped[str] = mapped_column(default="") """Message visible to anyone who can see the event""" title: Mapped[str] """Title of the event""" - signup_before: Mapped[Optional[datetime_tz]] + signup_before: Mapped[datetime_tz] """Time until signup is closed""" creator_user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) @@ -294,7 +294,7 @@ def get_enrollment_right(self, user: User) -> EnrollmentRight: .all() ) can_view_event_doc = False - if self.origin_doc_id: + if self.origin_doc: can_view_event_doc = verify_view_access( self.origin_doc, require=False, user=user ) diff --git a/timApp/plugin/group_join/group_join.py b/timApp/plugin/group_join/group_join.py index 08023b127c..647f41ff6b 100644 --- a/timApp/plugin/group_join/group_join.py +++ b/timApp/plugin/group_join/group_join.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import Callable +from typing import Callable, Sequence from flask import render_template_string, Response from marshmallow import missing @@ -105,8 +105,9 @@ def leave_groups(groups: list[str]) -> Response: current_user = get_current_user_object() def do_leave(user: User, group: UserGroup) -> None: - membership: UserGroupMember = user.active_memberships.get(group.id) - membership.set_expired() + membership: UserGroupMember | None = user.active_memberships.get(group.id) + if membership: + membership.set_expired() all_ok, _, result = _do_group_op( groups, current_user, "leave", True, lambda i: i.canLeave, do_leave @@ -120,7 +121,9 @@ def do_leave(user: User, group: UserGroup) -> None: def _check_self_join( group: UserGroup, user: User, check: Callable[[GroupSelfJoinSettings], bool] ) -> bool: - admin_doc: DocEntry = group.admin_doc.docentries[0] if group.admin_doc else None + admin_doc: DocEntry | None = ( + group.admin_doc.docentries[0] if group.admin_doc else None + ) if admin_doc is None: return False self_join_info = admin_doc.document.get_settings().group_self_join_info() @@ -144,7 +147,7 @@ def _do_group_op( ) result = dict.fromkeys(groups, "") - ugs: list[UserGroup] = ( + ugs: Sequence[UserGroup] = ( run_sql(select(UserGroup).filter(UserGroup.name.in_(groups))).scalars().all() ) diff --git a/timApp/plugin/importdata/__init__.py b/timApp/plugin/importdata/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/plugin/importdata/importData.py b/timApp/plugin/importdata/importData.py index 94f51e3e6a..1581d04eb1 100644 --- a/timApp/plugin/importdata/importData.py +++ b/timApp/plugin/importdata/importData.py @@ -377,17 +377,17 @@ def answer(args: ImportDataAnswerModel) -> PluginAnswerResp: ) users = {c: u for c, u in run_sql(stmt)} elif id_prop == "username": - stmt = select(User).filter(User.name.in_(idents)) - users = {u.name: u for u in run_sql(stmt).scalars()} + username_stmt = select(User).filter(User.name.in_(idents)) + users = {u.name: u for u in run_sql(username_stmt).scalars()} elif id_prop == "id": try: - stmt = select(User).filter(User.id.in_([int(i) for i in idents])) + id_stmt = select(User).filter(User.id.in_([int(i) for i in idents])) except ValueError as e: return args.make_answer_error(f"User ids must be ints ({e})") - users = {str(u.id): u for u in run_sql(stmt).scalars()} + users = {str(u.id): u for u in run_sql(id_stmt).scalars()} elif id_prop == "email": - stmt = select(User).filter(User.email.in_(idents)) - users = {u.email: u for u in run_sql(stmt).scalars()} + email_stmt = select(User).filter(User.email.in_(idents)) + users = {u.email: u for u in run_sql(email_stmt).scalars()} else: return args.make_answer_error( f"Invalid joinProperty: {args.markup.joinProperty}" diff --git a/timApp/plugin/jsrunner/util.py b/timApp/plugin/jsrunner/util.py index 1ffe2be3b9..3756e36ba3 100644 --- a/timApp/plugin/jsrunner/util.py +++ b/timApp/plugin/jsrunner/util.py @@ -3,9 +3,9 @@ from collections import defaultdict from dataclasses import dataclass, field from datetime import datetime -from typing import TypedDict, Any, DefaultDict, Literal +from typing import TypedDict, Any, DefaultDict, Literal, Sequence -from sqlalchemy import func, select +from sqlalchemy import func, select, Row from timApp.answer.answer import Answer from timApp.answer.answers import get_global_answers @@ -78,7 +78,7 @@ def handle_jsrunner_groups(groupdata: JsrunnerGroups | None, curr_user: User) -> group_members_state[ug] = UserGroupMembersState( before=current_state, after=set(current_state) ) - users: list[User] = ( + users: Sequence[User] = ( run_sql(select(User).filter(User.id.in_(uids))).scalars().all() ) found_user_ids = {u.id for u in users} @@ -353,7 +353,7 @@ def save_fields( .with_only_columns(func.max(Answer.id).label("aid"), User.id.label("uid")) .subquery() ) - datas: list[tuple[int, Answer]] = run_sql( + datas: Sequence[Row[tuple[int, Answer]]] = run_sql( select(Answer) .join(sq, Answer.id == sq.c.aid) .with_only_columns(sq.c.uid, Answer) diff --git a/timApp/plugin/reviewcanvas/__init__.py b/timApp/plugin/reviewcanvas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/plugin/tableform/__init__.py b/timApp/plugin/tableform/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/plugin/tableform/tableForm.py b/timApp/plugin/tableform/tableForm.py index 6edd8f6c15..e19ca1be16 100644 --- a/timApp/plugin/tableform/tableForm.py +++ b/timApp/plugin/tableform/tableForm.py @@ -29,6 +29,7 @@ from timApp.plugin.tableform.comparatorFilter import RegexOrComparator from timApp.plugin.taskid import TaskId from timApp.sisu.parse_display_name import parse_sisu_group_display_name +from timApp.sisu.scimusergroup import ScimUserGroup from timApp.sisu.sisu import get_potential_groups from timApp.tim_app import csrf from timApp.timdb.sqa import run_sql @@ -170,6 +171,7 @@ class TableFormInputModel: def get_sisu_group_desc_for_table(g: UserGroup) -> str: p = parse_sisu_group_display_name(g.display_name) assert p is not None + assert g.external_id is not None if g.external_id.is_studysubgroup: return p.desc # We want the most important groups to be at the top of the table. @@ -202,9 +204,17 @@ def get_course_page(ug: UserGroup) -> str | None: else: return None + def get_ext_id(g: UserGroup) -> ScimUserGroup: + assert g.external_id is not None + return g.external_id + + def get_display_name(g: UserGroup) -> str: + assert g.display_name is not None + return g.display_name + return TableFormObj( rows={ - g.external_id.external_id: { + get_ext_id(g).external_id: { "TIM-nimi": g.name, "URL": f'URL' if g.admin_doc @@ -215,10 +225,10 @@ def get_course_page(ug: UserGroup) -> str | None: for g in gs }, users={ - g.external_id.external_id: TableFormUserInfo( + get_ext_id(g).external_id: TableFormUserInfo( real_name=get_sisu_group_desc_for_table(g) if sisu_id - else g.display_name, + else get_display_name(g), # The rows are not supposed to match any real user when handling sisu groups, # so we try to use an id value that does not match anyone. id=-100000, @@ -233,7 +243,7 @@ def get_course_page(ug: UserGroup) -> str | None: "Jäseniä": "Jäseniä", "Kurssisivu": "Kurssisivu", }, - styles={g.external_id.external_id: {} for g in gs}, + styles={get_ext_id(g).external_id: {} for g in gs}, membership_add={}, membership_end={}, ) diff --git a/timApp/plugin/tape/__init__.py b/timApp/plugin/tape/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/plugin/timmenu/__init__.py b/timApp/plugin/timmenu/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/plugin/userselect/userselect.py b/timApp/plugin/userselect/userselect.py index b0c2666f7f..eae941d1e6 100644 --- a/timApp/plugin/userselect/userselect.py +++ b/timApp/plugin/userselect/userselect.py @@ -517,13 +517,13 @@ def undo_field_actions( def get_groups( cur_user: User, add: list[str], remove: list[str], change_all_groups: list[str] ) -> tuple[list[UserGroup], list[UserGroup], list[UserGroup]]: - add_groups: list[UserGroup] = ( + add_groups: list[UserGroup] = list( run_sql(select(UserGroup).filter(UserGroup.name.in_(add))).scalars().all() ) - remove_groups: list[UserGroup] = ( + remove_groups: list[UserGroup] = list( run_sql(select(UserGroup).filter(UserGroup.name.in_(remove))).scalars().all() ) - change_all_groups_ugs: list[UserGroup] = ( + change_all_groups_ugs: list[UserGroup] = list( run_sql(select(UserGroup).filter(UserGroup.name.in_(change_all_groups))) .scalars() .all() diff --git a/timApp/scheduling/__init__.py b/timApp/scheduling/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/scheduling/scheduling_routes.py b/timApp/scheduling/scheduling_routes.py index 78359eae19..a6e9c3825a 100644 --- a/timApp/scheduling/scheduling_routes.py +++ b/timApp/scheduling/scheduling_routes.py @@ -1,6 +1,6 @@ from dataclasses import field, dataclass from datetime import datetime -from typing import Any, Generator +from typing import Any, Generator, Sequence from flask import current_app, Response from isodate import Duration @@ -81,7 +81,7 @@ def get_scheduled_functions(all_users: bool = False) -> Response: if not all_users: stmt = stmt.filter(BlockAccess.block_id.in_(get_owned_objects_query(u))) - scheduled_fns: list[PeriodicTask] = run_sql(stmt).scalars().all() + scheduled_fns: Sequence[PeriodicTask] = run_sql(stmt).scalars().all() docentries = ( run_sql( @@ -96,16 +96,26 @@ def get_scheduled_functions(all_users: bool = False) -> Response: def gen() -> Generator[ScheduledFunctionItem, None, None]: for t in scheduled_fns: + block_id = t.block_id + expires = t.expires + interval = t.interval + block = t.block + + assert block_id is not None + assert expires is not None + assert interval is not None + assert block is not None + yield ScheduledFunctionItem( - block_id=t.block_id, + block_id=block_id, name=t.task_id.task_name, - expires=t.expires, + expires=expires, last_run_at=t.last_run_at, total_run_count=t.total_run_count, - interval=Interval(every=t.interval.every, period=t.interval.period), - owners=t.block.owners, + interval=Interval(every=interval.every, period=interval.period or ""), + owners=block.owners, doc_path=d_map[t.task_id.doc_id].path, - enabled=t.enabled, + enabled=t.enabled or False, ) return json_response(list(gen()), date_conversion=True) @@ -183,7 +193,7 @@ def add_scheduled_function( def delete_scheduled_plugin_run( function_id: int, ) -> Response: - pto: PeriodicTask = ( + pto: PeriodicTask | None = ( ( run_sql( select(PeriodicTask) @@ -198,6 +208,7 @@ def delete_scheduled_plugin_run( ) if not pto: raise NotExist("scheduled function not found") + assert pto.block is not None u = get_current_user_object() if not u.has_manage_access(pto.block) and not u.is_admin: raise AccessDenied() diff --git a/timApp/sisu/parse_display_name.py b/timApp/sisu/parse_display_name.py index 39eacd42aa..caa901eb28 100644 --- a/timApp/sisu/parse_display_name.py +++ b/timApp/sisu/parse_display_name.py @@ -57,7 +57,9 @@ def desc_slug(self) -> str: return desc -def parse_sisu_group_display_name(s: str) -> SisuDisplayName | None: +def parse_sisu_group_display_name(s: str | None) -> SisuDisplayName | None: + if not s: + return None m = display_name_re.fullmatch(s) if not m: return None diff --git a/timApp/sisu/scim.py b/timApp/sisu/scim.py index 87a1a88dd8..e45c888bcf 100644 --- a/timApp/sisu/scim.py +++ b/timApp/sisu/scim.py @@ -2,7 +2,7 @@ import traceback from dataclasses import field, dataclass from functools import cached_property -from typing import Any, Generator +from typing import Any, Generator, Sequence from flask import Blueprint, request, current_app, Response from sqlalchemy import select @@ -319,6 +319,7 @@ def put_group(group_id: str) -> Response: @scim.delete("/Groups/") def delete_group(group_id: str) -> Response: ug = get_group_by_scim(group_id) + assert ug.external_id is not None ug.name = f"{DELETED_GROUP_PREFIX}{ug.external_id.external_id}" db.session.delete(ug.external_id) db.session.commit() @@ -390,7 +391,7 @@ def update_users(ug: UserGroup, args: SCIMGroupModel) -> None: added_users = set() scimuser = User.get_scimuser() - existing_accounts: list[User] = ( + existing_accounts: Sequence[User] = ( run_sql( select(User).filter( User.name.in_(current_usernames) | User.email.in_(emails) @@ -534,7 +535,7 @@ def parse_sisu_group_display_name_or_error(args: SCIMGroupModel) -> SisuDisplayN def raise_conflict_error(args: SCIMGroupModel, e: IntegrityError) -> None: - msg = e.orig.diag.message_detail + msg = e.orig.diag.message_detail if e.orig else "" # type: ignore m = email_error_re.fullmatch(msg) if m: em = m.group("email") diff --git a/timApp/sisu/sisu.py b/timApp/sisu/sisu.py index 2de52f04e0..a63a73bbb3 100644 --- a/timApp/sisu/sisu.py +++ b/timApp/sisu/sisu.py @@ -74,12 +74,17 @@ def get_potential_groups_route() -> Response: u = get_current_user_object() result = get_potential_groups(u) + + def get_external_id(g: UserGroup) -> ScimUserGroup: + assert g.external_id is not None + return g.external_id + return json_response( [ { "id": g.id, "name": g.name, - "external_id": g.external_id.external_id, + "external_id": get_external_id(g).external_id, "display_name": g.display_name, "doc": g.admin_doc.docentries[0] if g.admin_doc else None, } @@ -99,7 +104,7 @@ def get_potential_groups_route() -> Response: def get_group_prefix(g: UserGroup) -> str | None: """Returns the prefix indicating which Sisu groups the users in this Sisu group shall have access to.""" - eid = g.external_id.external_id + eid = g.scim_user_group.external_id for s in role_suffixes: if eid.endswith(f"-{s}"): return eid[: -len(s)] + "%" @@ -111,11 +116,11 @@ def get_potential_groups(u: User, course_filter: str | None = None) -> list[User sisu_group_memberships = ( u.groups_dyn.join(UserGroup).join(ScimUserGroup).with_entities(UserGroup).all() ) - ug_filter = true() + ug_filter: Any = true() if not u.is_admin: accessible_prefixes = [get_group_prefix(g) for g in sisu_group_memberships] ug_filter = ug_filter & ScimUserGroup.external_id.like( - any_(accessible_prefixes) + any_(accessible_prefixes) # type: ignore ) if course_filter: ug_filter = ug_filter & ScimUserGroup.external_id.startswith( @@ -136,9 +141,10 @@ class GroupCreateModel: def get_sisu_group_rights(g: UserGroup) -> list[UserGroup]: group_names = [] - if g.external_id.is_studysubgroup: - group_names.append(g.external_id.without_role + "teachers") - course_code = g.external_id.course_id + external_id = g.scim_user_group + if external_id.is_studysubgroup: + group_names.append(external_id.without_role + "teachers") + course_code = external_id.course_id for r in role_suffixes: group_names.append(course_code + "-" + r) return get_sisu_groups_by_filter(ScimUserGroup.external_id.in_(group_names)) @@ -151,7 +157,7 @@ def create_groups_route(args: list[GroupCreateModel]) -> Response: # First, make sure user is eligible for access to all the requested groups. allowed_groups = get_potential_groups(u) - allowed_external_ids = {g.external_id.external_id for g in allowed_groups} + allowed_external_ids = {g.scim_user_group.external_id for g in allowed_groups} requested_external_ids = {a.externalId for a in args} not_allowed = requested_external_ids - allowed_external_ids if not_allowed: @@ -163,7 +169,7 @@ def create_groups_route(args: list[GroupCreateModel]) -> Response: # Rights to already existing documents need to be updated too. name_map: dict[str, str | Missing] = {a.externalId: a.name for a in args} group_map: dict[str, UserGroup] = { - g.external_id.external_id: g for g in allowed_groups + g.scim_user_group.external_id: g for g in allowed_groups } created = [] updated = [] @@ -171,7 +177,7 @@ def create_groups_route(args: list[GroupCreateModel]) -> Response: for r in requested_external_ids: g = group_map[r] name_m = name_map[r] - if not name_m: + if not isinstance(name_m, str): name = g.name else: name = name_m @@ -186,7 +192,7 @@ def create_groups_route(args: list[GroupCreateModel]) -> Response: ) expected_location = p.group_doc_root if g.admin_doc: - doc = g.admin_doc.docentries[0] + doc: DocInfo = g.admin_doc.docentries[0] doc.title = name # In theory, the admin doc can have multiple aliases, so we'll only update the one in the official location. for d in g.admin_doc.docentries: @@ -248,7 +254,8 @@ def create_sisu_document( def refresh_sisu_grouplist_doc(ug: UserGroup) -> None: - if not ug.external_id.is_student and not ug.external_id.is_studysubgroup: + external_id = ug.scim_user_group + if not external_id.is_student and not external_id.is_studysubgroup: gn = parse_sisu_group_display_name(ug.display_name) assert gn is not None sp = gn.sisugroups_doc_path @@ -256,7 +263,7 @@ def refresh_sisu_grouplist_doc(ug: UserGroup) -> None: settings_to_set = { "global_plugin_attrs": { "all": { - "sisugroups": ug.external_id.course_id, + "sisugroups": external_id.course_id, } }, "macros": { @@ -295,7 +302,7 @@ def refresh_sisu_grouplist_doc(ug: UserGroup) -> None: continue if not group.external_id: continue - if group.external_id.course_id != ug.external_id.course_id: + if group.external_id.course_id != external_id.course_id: continue doc.block.add_rights([ug], AccessType.owner) @@ -307,7 +314,7 @@ def refresh_sisu_grouplist_doc(ug: UserGroup) -> None: a = g_attrs.get("all") if isinstance(a, dict): sisugroups = a.get("sisugroups") - if sisugroups == ug.external_id.course_id: + if sisugroups == external_id.course_id: has_sisu_attr = True valid_settings = isinstance(sisugroups, str) if has_sisu_attr: @@ -322,7 +329,7 @@ def refresh_sisu_grouplist_doc(ug: UserGroup) -> None: plug = Plugin.from_paragraph(p, default_view_ctx) except PluginException: continue - if plug.values.get("sisugroups") == ug.external_id.course_id: + if plug.values.get("sisugroups") == external_id.course_id: return d.document.modifier_group_id = get_admin_group_id() d.document.add_text( @@ -330,7 +337,7 @@ def refresh_sisu_grouplist_doc(ug: UserGroup) -> None: # Sisu groups for course {gn.coursecode_and_time} ``` {{#table_extra plugin="tableForm"}} -sisugroups: {ug.external_id.course_id} +sisugroups: {external_id.course_id} table: true showInView: true report: false @@ -714,10 +721,10 @@ def get_sisu_assessments( raise AccessDenied("You are not a TIM teacher.") pot_groups = get_potential_groups(teacher, course_filter=sisu_id) if not any( - g.external_id.course_id == sisu_id + g.scim_user_group.course_id == sisu_id and ( - g.external_id.is_responsible_teacher - or g.external_id.is_administrative_person + g.scim_user_group.is_responsible_teacher + or g.scim_user_group.is_administrative_person ) for g in pot_groups ): @@ -739,7 +746,7 @@ def get_sisu_assessments( usergroups = groups_setting else: usergroups = groups - ugs = ( + ugs = list( run_sql(select(UserGroup).filter(UserGroup.name.in_(usergroups))) .scalars() .all() diff --git a/timApp/static/scripts/tim/messaging/manage-read-receipt.component.ts b/timApp/static/scripts/tim/messaging/manage-read-receipt.component.ts index 1ba9f9ea74..7a0db7db68 100644 --- a/timApp/static/scripts/tim/messaging/manage-read-receipt.component.ts +++ b/timApp/static/scripts/tim/messaging/manage-read-receipt.component.ts @@ -184,7 +184,7 @@ interface TimMessageReadReceipt { // Information about the read receipt retrieved from server message_id: number; user_id: number; - marked_as_read_on: Date; + marked_as_read_on?: Date; can_mark_as_read: boolean; } diff --git a/timApp/timdb/types.py b/timApp/timdb/types.py index 8fc9efd357..5d744e79bd 100644 --- a/timApp/timdb/types.py +++ b/timApp/timdb/types.py @@ -1,6 +1,8 @@ from datetime import datetime +from typing import TYPE_CHECKING -from flask_sqlalchemy.model import Model +# flask_sqlalchemy stubs are not up-to-date to support mypy >1.0 +from flask_sqlalchemy.model import Model # type: ignore from sqlalchemy import Text, DateTime from sqlalchemy.orm import DeclarativeBase, declared_attr, has_inherited_table from typing_extensions import Annotated @@ -18,8 +20,11 @@ class DbModel(DeclarativeBase, Model): datetime_tz: DateTime(timezone=True), } - @declared_attr.directive - def __tablename__(cls) -> str | None: - if has_inherited_table(cls): - return None - return cls.__name__.lower() + # Add check for mypy to suppress __tablename__ error when it's overriden as a string and not a method + if not TYPE_CHECKING: + + @declared_attr.directive + def __tablename__(cls) -> str | None: + if has_inherited_table(cls): + return None + return cls.__name__.lower() diff --git a/timApp/user/settings/__init__.py b/timApp/user/settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/user/settings/settings.py b/timApp/user/settings/settings.py index a66d5fe6a3..c6ac8ddff5 100644 --- a/timApp/user/settings/settings.py +++ b/timApp/user/settings/settings.py @@ -1,6 +1,6 @@ """Routes for settings view.""" from dataclasses import field -from typing import Any +from typing import Any, Sequence from flask import render_template, flash, Response from flask import request @@ -57,7 +57,7 @@ def verify_new_styles(curr_prefs: Preferences, new_prefs: Preferences) -> None: if not new_style_doc_ids: return - new_style_docs: list[DocEntry] = ( + new_style_docs: Sequence[DocEntry] = ( run_sql(select(DocEntry).filter(DocEntry.id.in_(new_style_doc_ids))) .scalars() .all() diff --git a/timApp/user/settings/styles.py b/timApp/user/settings/styles.py index 23a43c4075..5c5a2f8aa1 100644 --- a/timApp/user/settings/styles.py +++ b/timApp/user/settings/styles.py @@ -2,6 +2,7 @@ from io import StringIO from os.path import getmtime from pathlib import Path +from typing import Any import sass from flask import Response, current_app, flash @@ -298,7 +299,7 @@ def get_styles( """ cur_user = get_current_user_object() filter_user: User | None = cur_user - filter = DocEntry.name.like(f"{OFFICIAL_STYLES_PATH}%") | DocEntry.name.like( + filter: Any = DocEntry.name.like(f"{OFFICIAL_STYLES_PATH}%") | DocEntry.name.like( f"{USER_STYLES_PATH}%" ) @@ -368,7 +369,7 @@ def generate( """ verify_logged_in() - doc_entries: list[DocEntry] = ( + doc_entries: list[DocEntry] = list( run_sql(select(DocEntry).filter(DocEntry.id.in_(docs))).scalars().all() ) diff --git a/timApp/user/usergroup.py b/timApp/user/usergroup.py index 0faed0400f..622149f94f 100644 --- a/timApp/user/usergroup.py +++ b/timApp/user/usergroup.py @@ -167,6 +167,12 @@ def scim_modified(self): def scim_id(self): return self.external_id.external_id if self.external_id else None + @property + def scim_user_group(self) -> "ScimUserGroup": + if not self.external_id: + raise Exception(f"UserGroup {self.name} has no SCIM user group associated") + return self.external_id + @property def scim_resource_type(self): return "Group" diff --git a/timApp/util/file_utils.py b/timApp/util/file_utils.py index 99fc4299ba..2ba409f01e 100644 --- a/timApp/util/file_utils.py +++ b/timApp/util/file_utils.py @@ -1,7 +1,7 @@ from mimetypes import guess_extension from os import PathLike -import magic +import magic # type: ignore def guess_image_type(p: PathLike | str | bytes) -> str | None: @@ -23,4 +23,9 @@ def guess_image_type(p: PathLike | str | bytes) -> str | None: if not mime.startswith("image/"): return None - return guess_extension(mime).lstrip(".") + ext = guess_extension(mime) + + if not ext: + return None + + return ext.lstrip(".") diff --git a/timApp/util/flask/typedblueprint.py b/timApp/util/flask/typedblueprint.py index f31385fdae..b727f2cf62 100644 --- a/timApp/util/flask/typedblueprint.py +++ b/timApp/util/flask/typedblueprint.py @@ -152,7 +152,7 @@ def extract_and_call_by_kwargs(*args: Any, **kwargs: Any) -> Any: return func( *args, **kwargs, - **{f.name: getattr(extracted, f.name) for f in fields(extracted)}, + **{f.name: getattr(extracted, f.name) for f in fields(extracted)}, # type: ignore[arg-type] ) @wraps(func) diff --git a/timApp/util/get_fields.py b/timApp/util/get_fields.py index 7b588c5e5c..508d9b78d6 100644 --- a/timApp/util/get_fields.py +++ b/timApp/util/get_fields.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from datetime import datetime from enum import Enum, unique -from typing import Optional, DefaultDict, TypedDict, Any, Union +from typing import Optional, DefaultDict, TypedDict, Any, Union, NotRequired import attr import dateutil.parser @@ -122,6 +122,7 @@ class UserFieldObj(TypedDict): user: User fields: UserFields styles: Any + groupinfo: NotRequired[Any] @dataclass @@ -465,8 +466,12 @@ def get_fields_and_users( user_fieldstyles = {} user = users[user_index] assert user.id == uid - obj = {"user": user, "fields": user_tasks, "styles": user_fieldstyles} - res.append(UserFieldObj(**obj)) + obj = UserFieldObj( + user=user, + fields=user_tasks, + styles=user_fieldstyles, + ) + res.append(obj) m_add = get_membership_added(user, group_id_set) m_end = ( get_membership_end(user, group_id_set) diff --git a/timApp/velp/annotation.py b/timApp/velp/annotation.py index 0feac1a80f..0dbf225a9a 100644 --- a/timApp/velp/annotation.py +++ b/timApp/velp/annotation.py @@ -130,7 +130,10 @@ def check_visibility_and_maybe_get_doc( return True, d if ann.visible_to == AnnotationVisibility.everyone.value: return True, d - d = get_doc_or_abort(ann.document_id) + doc_id = ann.document_id + if doc_id is None: + doc_id = -1 + d = get_doc_or_abort(doc_id) if ( ann.visible_to == AnnotationVisibility.teacher.value and user.has_teacher_access(d) @@ -150,7 +153,10 @@ def check_annotation_edit_access_and_maybe_get_doc( if user.id == ann.annotator_id: return True, d if not d: - d = get_doc_or_abort(ann.document_id) + doc_id = ann.document_id + if doc_id is None: + doc_id = -1 + d = get_doc_or_abort(doc_id) verify_teacher_access(d) return True, d @@ -181,7 +187,10 @@ def update_annotation( ann.color = color if not d: - d = get_doc_or_abort(ann.document_id) + doc_id = ann.document_id + if doc_id is None: + doc_id = -1 + d = get_doc_or_abort(doc_id) if has_teacher_access(d): ann.points = points if coord: @@ -251,7 +260,10 @@ def add_comment_route(id: int, content: str) -> Response: if not content: raise RouteException("Comment must not be empty") if not d: - d = get_doc_or_abort(a.document_id) + doc_id = a.document_id + if doc_id is None: + doc_id = -1 + d = get_doc_or_abort(doc_id) a.comments.append(AnnotationComment(content=content, commenter_id=commenter.id)) # TODO: Send email to annotator if commenter is not the annotator. db.session.commit() diff --git a/timApp/velp/annotations.py b/timApp/velp/annotations.py index fee356725b..35e15c061c 100644 --- a/timApp/velp/annotations.py +++ b/timApp/velp/annotations.py @@ -8,6 +8,7 @@ """ from enum import Enum, unique +from typing import Any from sqlalchemy import func, true, select from sqlalchemy.orm import selectinload, joinedload @@ -57,8 +58,8 @@ def get_annotations_with_comments_in_document( vis_filter = vis_filter | ( Annotation.visible_to == AnnotationVisibility.owner.value ) - answer_filter = true() - own_review_filter = true() + answer_filter: Any = true() + own_review_filter: Any = true() if not user.has_seeanswers_access(d) or only_own: answer_filter = (User.id == user.id) | (User.id == None) if is_peerreview_enabled(d): @@ -97,7 +98,7 @@ def get_annotations_with_comments_in_document( .options(joinedload(Annotation.answer).selectinload(Answer.users_all)) .with_only_columns(Annotation) ) - anns = run_sql(q).scalars().all() + anns = list(run_sql(q).scalars().all()) return anns diff --git a/timApp/velp/velp.py b/timApp/velp/velp.py index 215222fc86..b26407ffe5 100644 --- a/timApp/velp/velp.py +++ b/timApp/velp/velp.py @@ -9,6 +9,8 @@ :version: 1.0.0 """ +from typing import Sequence + from flask import Blueprint, Response from flask import request from sqlalchemy import select, delete @@ -169,7 +171,7 @@ def get_default_velp_group(doc_id: int) -> Response: for v in found_velp_groups: # if has_view_access(user_id, timdb.documents.get_document_id(v['name'])): velp_groups.append(v.id) - def_velp_groups: list[VelpGroup] = ( + def_velp_groups: Sequence[VelpGroup] = ( run_sql( select(VelpGroup).filter( VelpGroup.id.in_(velp_groups) & VelpGroup.default_group == True @@ -708,7 +710,7 @@ def reset_all_selections_to_defaults(doc_id: int) -> Response: user_id = get_current_user_id() run_sql( - delete(VelpGroupSelection).filter_by( + delete(VelpGroupSelection).where( (VelpGroupSelection.doc_id == doc_id) & (VelpGroupSelection.user_id == user_id) ) diff --git a/timApp/velp/velpgroups.py b/timApp/velp/velpgroups.py index 087ac90f8b..41c533ae52 100644 --- a/timApp/velp/velpgroups.py +++ b/timApp/velp/velpgroups.py @@ -10,7 +10,7 @@ """ from dataclasses import dataclass, field -from typing import Union +from typing import Union, Sequence from sqlalchemy import select, delete @@ -199,7 +199,7 @@ def get_groups_from_document_table(doc_id: int, user_id: int | None) -> list[Vel """ if not user_id: - return ( + return list( run_sql( select(VelpGroupsInDocument) .filter_by(doc_id=doc_id) @@ -209,7 +209,7 @@ def get_groups_from_document_table(doc_id: int, user_id: int | None) -> list[Vel .scalars() .all() ) - return ( + return list( run_sql( select(VelpGroupsInDocument) .filter_by(user_id=user_id, doc_id=doc_id) @@ -256,7 +256,7 @@ def add_groups_to_document( velp_groups: list[VelpGroupOrDocInfo], doc: DocInfo, user: User ) -> None: """Adds velp groups to VelpGroupsInDocument table.""" - existing: list[VelpGroupsInDocument] = ( + existing: Sequence[VelpGroupsInDocument] = ( run_sql(select(VelpGroupsInDocument).filter_by(user_id=user.id, doc_id=doc.id)) .scalars() .all() @@ -337,7 +337,7 @@ def change_all_target_area_default_selections( & (VelpGroupDefaults.target_id == target_id) ) ) - vgids: list[VelpGroupsInDocument] = ( + vgids: Sequence[VelpGroupsInDocument] = ( run_sql(select(VelpGroupsInDocument).filter_by(doc_id=doc_id, user_id=user_id)) .scalars() .all() @@ -387,7 +387,7 @@ def change_all_target_area_selections( ) # target_type is 0 because only 0 always contains all velp groups user has access to. # Other target types will get added to database only after they've been clicked once in interface. - vgss: list[VelpGroupSelection] = ( + vgss: Sequence[VelpGroupSelection] = ( run_sql( select(VelpGroupSelection).filter_by( doc_id=doc_id, @@ -422,7 +422,7 @@ def change_default_selection( :param selected: Boolean whether group is selected or not """ - vgd: VelpGroupDefaults = ( + vgd: VelpGroupDefaults | None = ( run_sql( select(VelpGroupDefaults) .filter_by( @@ -496,7 +496,7 @@ def process_selection_info( while i < len(vgss): if target_id == vgss[i].target_id: selection = GroupSelection( - id=vgss[i].velp_group_id, selected=vgss[i].selected + id=vgss[i].velp_group_id, selected=vgss[i].selected or False ) groups.append(target_id, selection) i += 1 @@ -516,7 +516,7 @@ def get_personal_selections_for_velp_groups( :return: Dict with following info { target_id: [{velp_group_id, selected}, etc], etc } """ - vgss = ( + vgss = list( run_sql( select(VelpGroupSelection) .filter_by(doc_id=doc_id, user_id=user_id) @@ -537,7 +537,7 @@ def get_default_selections_for_velp_groups( :return: Dict with following info { target_id: [{velp_group_id, selected}, etc], etc } """ - vgds = ( + vgds = list( run_sql( select(VelpGroupDefaults) .filter_by(doc_id=doc_id) diff --git a/timApp/velp/velps.py b/timApp/velp/velps.py index c6689559eb..13c3c34aa9 100644 --- a/timApp/velp/velps.py +++ b/timApp/velp/velps.py @@ -139,7 +139,7 @@ def create_new_velp( def update_velp( - velp_id: int, default_points: str, color: str, visible_to: int, style: int + velp_id: int, default_points: float, color: str, visible_to: int, style: int ) -> None: """Changes the non-versioned properties of a velp. Does not update labels. @@ -254,12 +254,12 @@ def get_velp_content_for_document( .options(selectinload(Velp.groups).raiseload(VelpGroup.block)) .options(selectinload(Velp.velp_versions).joinedload(VelpVersion.content)) ) - return run_sql(vq).scalars().all() + return list(run_sql(vq).scalars().all()) def get_velp_label_content_for_document( doc_id: int, user_id: int, language_id: str = "FI" -) -> dict: +) -> list[VelpLabelContent]: """Gets velp label content for document. Uses VelpGroupsInDocument table data to determine which velp groups and via those which velp labels are usable @@ -271,7 +271,7 @@ def get_velp_label_content_for_document( :return: List of dicts containing velp label ids and content """ - vlcs = ( + vlcs = list( run_sql( select(VelpLabelContent) .filter_by(language_id=language_id) From a8f8b0179094095b93264581d8e3e798c6721adc Mon Sep 17 00:00:00 2001 From: dezhidki Date: Wed, 2 Aug 2023 16:17:27 +0300 Subject: [PATCH 19/34] Fix formatting --- timApp/document/editing/clipboard.py | 3 -- timApp/document/editing/routes_clipboard.py | 2 +- timApp/document/minutes/routes.py | 1 - .../document/translation/translationparser.py | 4 ++- timApp/lecture/question_utils.py | 2 +- .../98c571553028_add_event_important.py | 1 + ...14a0e59d3_add_message_verification_type.py | 1 + timApp/modules/cs/cs.py | 1 - timApp/modules/imagex/geometry.py | 2 -- timApp/modules/svn/svn3.py | 1 + .../printing/pandoc_imagefilepathsfilter.py | 1 - timApp/tests/server/test_importdata.py | 12 ++++---- timApp/tests/server/test_question.py | 16 +++++++--- .../tests/server/test_scheduled_functions.py | 30 ++++++++++--------- timApp/tests/server/test_scim.py | 4 +-- timApp/tests/server/test_showfile.py | 6 ++-- timApp/util/pdftools.py | 1 - tim_common/tim_server.py | 1 + 18 files changed, 48 insertions(+), 41 deletions(-) diff --git a/timApp/document/editing/clipboard.py b/timApp/document/editing/clipboard.py index fe038dc223..606208ea86 100644 --- a/timApp/document/editing/clipboard.py +++ b/timApp/document/editing/clipboard.py @@ -70,7 +70,6 @@ def read_metadata(self) -> dict[str, Any]: def read( self, as_ref: bool | None = False, force_parrefs: bool | None = False ) -> list[dict[str, str]] | None: - if as_ref: clipfilename = ( self.get_parreffilename() @@ -129,7 +128,6 @@ def cut_pars( par_end: str, area_name: str | None = None, ) -> list[DocParagraph]: - pars = self.copy_pars(doc, par_start, par_end, area_name, disable_ref=True) doc.delete_section(par_start, par_end) self.update_metadata(last_action="cut") @@ -143,7 +141,6 @@ def copy_pars( area_name: str | None = None, disable_ref: bool = False, ) -> list[DocParagraph]: - par_objs = doc.get_section(par_start, par_end) pars = [p.dict() for p in par_objs] cannot_see_source = any( diff --git a/timApp/document/editing/routes_clipboard.py b/timApp/document/editing/routes_clipboard.py index c20d0f28c1..265b274f33 100644 --- a/timApp/document/editing/routes_clipboard.py +++ b/timApp/document/editing/routes_clipboard.py @@ -142,7 +142,7 @@ def paste_from_clipboard( src_doc = None parrefs = clip.read(as_ref=True, force_parrefs=True) - for (src_par_dict, dest_par) in zip(parrefs, pars): + for src_par_dict, dest_par in zip(parrefs, pars): try: src_docid = int(src_par_dict["attrs"]["rd"]) src_parid = src_par_dict["attrs"]["rp"] diff --git a/timApp/document/minutes/routes.py b/timApp/document/minutes/routes.py index 702d7cc0ca..3ec525dad1 100644 --- a/timApp/document/minutes/routes.py +++ b/timApp/document/minutes/routes.py @@ -85,7 +85,6 @@ def create_minute_extracts(doc: str) -> Response: end_markdown = ")%%" for par in paragraphs: - if par.is_setting(): continue diff --git a/timApp/document/translation/translationparser.py b/timApp/document/translation/translationparser.py index 3b12603846..6613349ff7 100644 --- a/timApp/document/translation/translationparser.py +++ b/timApp/document/translation/translationparser.py @@ -88,6 +88,7 @@ NOTRANSLATE_STYLE_SHORT = "nt" """Shorter string used for marking non-translatable text in TIM's Markdown""" + # TODO This name is kinda bad. Better would be along the lines of # translate-flag or a whole new list-type data structure, that describes # alternating between Yes's and No's @@ -115,7 +116,8 @@ class NoTranslate(TranslateApproval): @dataclass class Table(TranslateApproval): """ - Hacky way to translate tables by identifying them at translation and setting html-tag handling on.""" + Hacky way to translate tables by identifying them at translation and setting html-tag handling on. + """ ... diff --git a/timApp/lecture/question_utils.py b/timApp/lecture/question_utils.py index 6535d3c6e5..f59013d853 100644 --- a/timApp/lecture/question_utils.py +++ b/timApp/lecture/question_utils.py @@ -136,7 +136,7 @@ def calculate_points_from_json_answer( default_points = 0 if points_table is None: points_table = [{}] * len(single_answers) - for (oneAnswer, point_row) in zip(single_answers, points_table): + for oneAnswer, point_row in zip(single_answers, points_table): for oneLine in oneAnswer: if oneLine in point_row: points += point_row[oneLine] diff --git a/timApp/migrations/versions/98c571553028_add_event_important.py b/timApp/migrations/versions/98c571553028_add_event_important.py index 2d037faaa7..4455c067ba 100644 --- a/timApp/migrations/versions/98c571553028_add_event_important.py +++ b/timApp/migrations/versions/98c571553028_add_event_important.py @@ -13,6 +13,7 @@ import sqlalchemy as sa from alembic import op + # noinspection SqlResolve def upgrade(): # ### commands auto generated by Alembic - please adjust! ### diff --git a/timApp/migrations/versions/a6614a0e59d3_add_message_verification_type.py b/timApp/migrations/versions/a6614a0e59d3_add_message_verification_type.py index 518af68966..29bd6e5ab8 100644 --- a/timApp/migrations/versions/a6614a0e59d3_add_message_verification_type.py +++ b/timApp/migrations/versions/a6614a0e59d3_add_message_verification_type.py @@ -17,6 +17,7 @@ "NONE", "FORWARD", "MUNGE_FROM", name="messageverificationtype" ) + # noinspection SqlResolve def upgrade(): # ### commands auto generated by Alembic - please adjust! ### diff --git a/timApp/modules/cs/cs.py b/timApp/modules/cs/cs.py index d8900b1c8b..8b4e501f65 100644 --- a/timApp/modules/cs/cs.py +++ b/timApp/modules/cs/cs.py @@ -1194,7 +1194,6 @@ def do_all(self, query): self.wout(str(e)) def do_all_t(self, query: QueryClass): - convert_graphviz(query) t1start = time.time() diff --git a/timApp/modules/imagex/geometry.py b/timApp/modules/imagex/geometry.py index d95d71bfab..a7b8c7ea7f 100644 --- a/timApp/modules/imagex/geometry.py +++ b/timApp/modules/imagex/geometry.py @@ -36,7 +36,6 @@ def is_inside(type, size, angle, center, point): # Class for rectangles class Rectangle: - # Create rectangle def __init__(self, size, angle, center): self.size = size @@ -72,7 +71,6 @@ def is_inside(self, point): # Class for ellipses class Ellipse: - # Initialize object def __init__(self, size, angle, center): # height, width, center and angle of the ellipse diff --git a/timApp/modules/svn/svn3.py b/timApp/modules/svn/svn3.py index 601019d805..7419971946 100755 --- a/timApp/modules/svn/svn3.py +++ b/timApp/modules/svn/svn3.py @@ -778,6 +778,7 @@ def run_while_true(server_class=http.server.HTTPServer, # Jos ajaa Linuxissa ThreadingMixIn, niin chdir vaihtaa kaikkien hakemistoa? # Ongelmaa korjattu siten, että kaikki run-kommennot saavat prgpathin käyttöönsä + # if __debug__: # if True: class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer): diff --git a/timApp/printing/pandoc_imagefilepathsfilter.py b/timApp/printing/pandoc_imagefilepathsfilter.py index e490764b06..fb2d98c902 100755 --- a/timApp/printing/pandoc_imagefilepathsfilter.py +++ b/timApp/printing/pandoc_imagefilepathsfilter.py @@ -270,7 +270,6 @@ def handle_images(key, value, fmt, meta): if __name__ == "__main__": - # Needs to import different package based on python version, as the urlparse method # was moved from urlparse module to urllib.parse between python2.7 -> python3 try: diff --git a/timApp/tests/server/test_importdata.py b/timApp/tests/server/test_importdata.py index 013782e7e5..95d49e2d1c 100644 --- a/timApp/tests/server/test_importdata.py +++ b/timApp/tests/server/test_importdata.py @@ -58,14 +58,14 @@ class ImportDataTestBase(TimRouteTest): def imp(self, d, data, expect, status: int, task=None, aalto_return=None): if not task: task = "t" - + def init_mock(m): m.add( - "GET", - "https://plus.cs.aalto.fi/api/v2/courses/1234/aggregatedata/?format=json", - body=json.dumps(aalto_return), - status=200, - ) + "GET", + "https://plus.cs.aalto.fi/api/v2/courses/1234/aggregatedata/?format=json", + body=json.dumps(aalto_return), + status=200, + ) self.post_answer( "importData", diff --git a/timApp/tests/server/test_question.py b/timApp/tests/server/test_question.py index e0234c4e27..378f5231fa 100644 --- a/timApp/tests/server/test_question.py +++ b/timApp/tests/server/test_question.py @@ -126,7 +126,9 @@ def test_question_html(self): "/getQuestionByParId", query_string={"doc_id": d.id, "par_id": pars[2].get_id()}, expect_status=400, - expect_content={"error": f"Paragraph is not a plugin: {pars[2].get_id()}"}, + expect_content={ + "error": f"Paragraph is not a plugin: {pars[2].get_id()}" + }, ) normal_par_id = pars[3].get_id() @@ -172,11 +174,15 @@ def test_hidden_points(self): - def """ ) - d.document.set_settings({"global_plugin_attrs": {"qst": {"showPoints": False}}}) + d.document.set_settings( + {"global_plugin_attrs": {"qst": {"showPoints": False}}} + ) self.test_user_2.grant_access(d, AccessType.view) db.session.commit() db.session.refresh(d) - r = self.post_answer("qst", f"{d.id}.t", user_input={"answers": [["2"], ["1"]]}) + r = self.post_answer( + "qst", f"{d.id}.t", user_input={"answers": [["2"], ["1"]]} + ) self.assertEqual( { "markup": { @@ -200,7 +206,9 @@ def test_hidden_points(self): answers = self.get_task_answers(f"{d.id}.t", self.current_user) self.assertEqual(0.5, answers[0]["points"]) self.login_test2() - r = self.post_answer("qst", f"{d.id}.t", user_input={"answers": [["2"], ["1"]]}) + r = self.post_answer( + "qst", f"{d.id}.t", user_input={"answers": [["2"], ["1"]]} + ) self.assertEqual( { "markup": { diff --git a/timApp/tests/server/test_scheduled_functions.py b/timApp/tests/server/test_scheduled_functions.py index f8edd1dcf0..1f9edb01d6 100644 --- a/timApp/tests/server/test_scheduled_functions.py +++ b/timApp/tests/server/test_scheduled_functions.py @@ -307,20 +307,22 @@ def test_import_function_with_create_users(self): m.add( "GET", "https://plus.cs.aalto.fi/api/v2/courses/1234/aggregatedata/?format=json", - body=json.dumps([ - { - "UserID": 123, - "StudentID": "12345X", - "Email": "matti.meikalainen@aalto.fi", - "Tags": "aalto", - "1 Count": 2, - "1 Total": 100, - "1 Ratio": 0.125, - "2 Count": 0, - "2 Total": 0, - "2 Ratio": 0.0, - } - ]), + body=json.dumps( + [ + { + "UserID": 123, + "StudentID": "12345X", + "Email": "matti.meikalainen@aalto.fi", + "Tags": "aalto", + "1 Count": 2, + "1 Total": 100, + "1 Ratio": 0.125, + "2 Count": 0, + "2 Total": 0, + "2 Ratio": 0.0, + } + ] + ), status=200, ) do_run_user_function( diff --git a/timApp/tests/server/test_scim.py b/timApp/tests/server/test_scim.py index c0ca55c7e9..c1026b705b 100644 --- a/timApp/tests/server/test_scim.py +++ b/timApp/tests/server/test_scim.py @@ -888,7 +888,7 @@ def test_potential_groups(self): ["ussg-s1", "ussg-s2"], ), ] - for (external_id, display_name, users) in entries: + for external_id, display_name, users in entries: self.json_post( "/scim/Groups", { @@ -1180,7 +1180,7 @@ def test_potential_groups(self): ) # Make sure there won't be duplicate mails for responsible teachers. - for (external_id, display_name, users) in [responsible_teachers]: + for external_id, display_name, users in [responsible_teachers]: self.json_put( f"/scim/Groups/{external_id}", { diff --git a/timApp/tests/server/test_showfile.py b/timApp/tests/server/test_showfile.py index 5ec8c148cb..11c479147a 100644 --- a/timApp/tests/server/test_showfile.py +++ b/timApp/tests/server/test_showfile.py @@ -29,7 +29,9 @@ def run_server(): file: http://tests:8080/ping """ ) - self.assert_content(self.get(d.url, as_tree=True), ['{"status":"ok"}\nping']) + self.assert_content( + self.get(d.url, as_tree=True), ['{"status":"ok"}\nping'] + ) finally: server.shutdown() t.join() @@ -46,5 +48,3 @@ def test_no_local_file_access(self): self.get(d.url, as_tree=True), ["URL scheme must be http or https, got 'file'something"], ) - - diff --git a/timApp/util/pdftools.py b/timApp/util/pdftools.py index 1e6f1c9e3c..5484e9f38d 100644 --- a/timApp/util/pdftools.py +++ b/timApp/util/pdftools.py @@ -746,7 +746,6 @@ def is_pdf_producer_ghostscript(f: UploadedFile): def compress_pdf_if_not_already(f: UploadedFile): - # If the PDF producer is Ghostscript, let's assume this PDF has already been compressed. # It's unlikely that any end user uses it. if is_pdf_producer_ghostscript(f): diff --git a/tim_common/tim_server.py b/tim_common/tim_server.py index 1406b52fe6..bbc3284f12 100644 --- a/tim_common/tim_server.py +++ b/tim_common/tim_server.py @@ -223,6 +223,7 @@ def log(request: TimServer): # Jos ajaa Linuxissa ThreadingMixIn, niin chdir vaihtaa kaikkien hakemistoa? # Ongelmaa korjattu siten, että kaikki run-kommennot saavat prgpathin käyttöönsä + # if __debug__: # if True: class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer): From 69e4e2dd297c57b3f596254194038c863cf44744 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Wed, 2 Aug 2023 16:17:41 +0300 Subject: [PATCH 20/34] Fix Python version in CI --- .github/workflows/format.yml | 2 +- .github/workflows/lint.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index c5be23babd..23c89c17d9 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -36,7 +36,7 @@ jobs: name: Check Python formatting runs-on: ubuntu-latest env: - PYTHON_VERSION: "3.10" + PYTHON_VERSION: "3.11" steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 776a8272e2..5b4eb03dc4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -46,7 +46,7 @@ jobs: name: Lint Python runs-on: ubuntu-latest env: - PYTHON_VERSION: "3.10" + PYTHON_VERSION: "3.11" needs: skip_check if: needs.skip_check.outputs.should_skip != 'true' From 6c579c40dd270ce4c8cc6a85e7b2eb30cced0602 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Wed, 2 Aug 2023 16:30:32 +0300 Subject: [PATCH 21/34] Fix tim-base image build target --- cli/commands/dev/build.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cli/commands/dev/build.py b/cli/commands/dev/build.py index 1a64ba790c..613108d6ca 100644 --- a/cli/commands/dev/build.py +++ b/cli/commands/dev/build.py @@ -27,7 +27,9 @@ # 2. Specify the task and valid tags in the BUILD_TASKS dictionary. -def build_tim(tag: Optional[str], no_cache: bool, build_args: List[str]) -> Optional[List[str]]: +def build_tim( + tag: Optional[str], no_cache: bool, build_args: List[str] +) -> Optional[List[str]]: config = get_config() image_suffix = "-base" if tag == "base" else "" image_name = f"{config.images_repository}/tim{image_suffix}" @@ -45,6 +47,8 @@ def build_tim(tag: Optional[str], no_cache: bool, build_args: List[str]) -> Opti "build", *(["--no-cache"] if no_cache else []), *build_args_cli, + "--target", + tag, "--tag", image_name_specific, "--tag", @@ -57,7 +61,9 @@ def build_tim(tag: Optional[str], no_cache: bool, build_args: List[str]) -> Opti return [image_name_specific, image_name_latest] -def build_csplugin(tag: Optional[str], no_cache: bool, build_args: List[str]) -> Optional[List[str]]: +def build_csplugin( + tag: Optional[str], no_cache: bool, build_args: List[str] +) -> Optional[List[str]]: assert tag is not None config = get_config() image_name = f"{config.images_repository}/cs3:{tag}-{csplugin_image_tag()}" @@ -162,4 +168,3 @@ def init(parser: ArgumentParser) -> None: choices=choices, help="Tasks to build in format `task:tag`. If not specified, all tasks will be built.", ) - From 4d18a162df1837dd086cf4049aedb136695bdb5f Mon Sep 17 00:00:00 2001 From: dezhidki Date: Fri, 4 Aug 2023 09:36:53 +0300 Subject: [PATCH 22/34] Use latest Chromedriver --- timApp/Dockerfile | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/timApp/Dockerfile b/timApp/Dockerfile index 52f059c51e..bddc34f876 100755 --- a/timApp/Dockerfile +++ b/timApp/Dockerfile @@ -192,13 +192,20 @@ RUN /usr/local/bin/wrap_chrome_binary ARG CHROME_DRIVER_VERSION RUN bash -c "${APT_INSTALL} jq && ${APT_CLEANUP}" -RUN if [ -z "$CHROME_DRIVER_VERSION" ]; \ - then CHROME_MAJOR_VERSION=$(google-chrome --version | sed -E "s/.* ([0-9]+)(\.[0-9]+){3}.*/\1/") \ - && CHROME_DRIVER_VERSION=$(wget -q -O - "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" | jq --arg majorVersion "$CHROME_MAJOR_VERSION" -r '.channels.Stable | select(.version | startswith($majorVersion | tostring)).version'); \ +RUN if [ ! -z "$CHROME_DRIVER_VERSION" ]; \ + then CHROME_DRIVER_URL=https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/$CHROME_DRIVER_VERSION/linux64/chromedriver-linux64.zip ; \ + else echo "Geting ChromeDriver binary from https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" \ + && CFT_URL=https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json \ + && CFT_CHANNEL="Stable" \ + && if [ "$(echo "$CHROME_VERSION" | grep -q "beta")" ]; then CFT_CHANNEL="Beta"; fi \ + && if [ "$(echo "$CHROME_VERSION" | grep -q "unstable")" ]; then CFT_CHANNEL="Dev"; fi \ + && CTF_VALUES=$(curl -sSL $CFT_URL | jq -r --arg CFT_CHANNEL "$CFT_CHANNEL" '.channels[] | select (.channel==$CFT_CHANNEL)') \ + && CHROME_DRIVER_VERSION=$(echo $CTF_VALUES | jq -r '.version' ) \ + && CHROME_DRIVER_URL=$(echo $CTF_VALUES | jq -r '.downloads.chromedriver[] | select(.platform=="linux64") | .url' ) ; \ fi \ - && echo "Using chromedriver version: "$CHROME_DRIVER_VERSION \ - && CHROME_DRIVER_URL=$(wget -q -O - "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" | jq --arg version "$CHROME_DRIVER_VERSION" -r '.channels.Stable | select(.version==$version).downloads.chromedriver[] | select(.platform == "linux64") | .url') \ - && wget -q -O /tmp/chromedriver_linux64.zip "$CHROME_DRIVER_URL" \ + && echo "Using ChromeDriver from: "$CHROME_DRIVER_URL \ + && echo "Using ChromeDriver version: "$CHROME_DRIVER_VERSION \ + && wget --no-verbose -O /tmp/chromedriver_linux64.zip $CHROME_DRIVER_URL \ && rm -rf /opt/selenium/chromedriver \ && unzip /tmp/chromedriver_linux64.zip -d /opt/selenium \ && rm /tmp/chromedriver_linux64.zip \ From b9838d6b6ec52632604cffafada7a4d9017a9fc9 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Fri, 4 Aug 2023 11:04:09 +0300 Subject: [PATCH 23/34] Fix typos --- timApp/Dockerfile | 2 +- timApp/messaging/messagelist/messagelist_models.py | 4 +--- timApp/tests/server/timroutetest.py | 2 +- timApp/timdb/types.py | 2 +- typos.toml | 1 + 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/timApp/Dockerfile b/timApp/Dockerfile index bddc34f876..5a4e2c40b3 100755 --- a/timApp/Dockerfile +++ b/timApp/Dockerfile @@ -194,7 +194,7 @@ ARG CHROME_DRIVER_VERSION RUN bash -c "${APT_INSTALL} jq && ${APT_CLEANUP}" RUN if [ ! -z "$CHROME_DRIVER_VERSION" ]; \ then CHROME_DRIVER_URL=https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/$CHROME_DRIVER_VERSION/linux64/chromedriver-linux64.zip ; \ - else echo "Geting ChromeDriver binary from https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" \ + else echo "Getting ChromeDriver binary from https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" \ && CFT_URL=https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json \ && CFT_CHANNEL="Stable" \ && if [ "$(echo "$CHROME_VERSION" | grep -q "beta")" ]; then CFT_CHANNEL="Beta"; fi \ diff --git a/timApp/messaging/messagelist/messagelist_models.py b/timApp/messaging/messagelist/messagelist_models.py index 5e92605687..26653fbecf 100644 --- a/timApp/messaging/messagelist/messagelist_models.py +++ b/timApp/messaging/messagelist/messagelist_models.py @@ -339,9 +339,7 @@ def is_personal_user(self) -> bool: return self.is_external_member() ug = ( - db.session.exectute( - select(UserGroup).filter_by(id=self.tim_member.group_id) - ) + db.session.execute(select(UserGroup).filter_by(id=self.tim_member.group_id)) .scalars() .one() ) diff --git a/timApp/tests/server/timroutetest.py b/timApp/tests/server/timroutetest.py index 405003a9d2..1d22b8d72b 100644 --- a/timApp/tests/server/timroutetest.py +++ b/timApp/tests/server/timroutetest.py @@ -162,7 +162,7 @@ def setUp(self): # FIXME: It is a VERY bad idea to enter a client context for the duration of the entire test # because the client is not multithreaded. See https://github.com/pallets/flask/issues/4734 - # Instead, the client contex should be entered only in specific tests and explicitly + # Instead, the client context should be entered only in specific tests and explicitly self.client = self.client.__enter__() self.client.open("/") del_g() diff --git a/timApp/timdb/types.py b/timApp/timdb/types.py index 5d744e79bd..1528831093 100644 --- a/timApp/timdb/types.py +++ b/timApp/timdb/types.py @@ -20,7 +20,7 @@ class DbModel(DeclarativeBase, Model): datetime_tz: DateTime(timezone=True), } - # Add check for mypy to suppress __tablename__ error when it's overriden as a string and not a method + # Add check for mypy to suppress __tablename__ error when it's overridden as a string and not a method if not TYPE_CHECKING: @declared_attr.directive diff --git a/typos.toml b/typos.toml index 08290b58eb..1d9dca7fc8 100644 --- a/typos.toml +++ b/typos.toml @@ -60,6 +60,7 @@ sectionning = "sectionning" eJwVy0EOhCAMQNG7dG0yahGFy5BaS2ZigEnFlfHu4vL95F8Q_qKJsuQKvuopHVDmb1HwAB0wJQlRS2r8vD40hlp2yS2YwTGamceerWXkaR0nIiTroiFaNtsPy = "eJwVy0EOhCAMQNG7dG0yahGFy5BaS2ZigEnFlfHu4vL95F8Q_qKJsuQKvuopHVDmb1HwAB0wJQlRS2r8vD40hlp2yS2YwTGamceerWXkaR0nIiTroiFaNtsPy" Versio = "Versio" MATA280 = "MATA280" +selectin = "selectin" braket = "braket" [default.extend-words] From 31a8c0f605ecf83cffe072981a69e34332dc7447 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Fri, 4 Aug 2023 13:29:24 +0300 Subject: [PATCH 24/34] Fix MyPy error in CLI --- cli/commands/dev/build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/commands/dev/build.py b/cli/commands/dev/build.py index 613108d6ca..e389dd77a7 100644 --- a/cli/commands/dev/build.py +++ b/cli/commands/dev/build.py @@ -30,6 +30,7 @@ def build_tim( tag: Optional[str], no_cache: bool, build_args: List[str] ) -> Optional[List[str]]: + assert tag is not None config = get_config() image_suffix = "-base" if tag == "base" else "" image_name = f"{config.images_repository}/tim{image_suffix}" From 8c7bca5ded4a2f53bda4766f5026feef9740e323 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Fri, 4 Aug 2023 13:35:14 +0300 Subject: [PATCH 25/34] Fix formatting --- timApp/answer/routes.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/timApp/answer/routes.py b/timApp/answer/routes.py index 217e7cfdf1..bf75d38b42 100644 --- a/timApp/answer/routes.py +++ b/timApp/answer/routes.py @@ -448,7 +448,13 @@ def get_useranswers_for_task( .subquery() ) answs: list[Answer] = ( - run_sql(select(Answer).join(sub, Answer.id == sub.c.col).options(selectinload(Answer.users_all))).scalars().all() + run_sql( + select(Answer) + .join(sub, Answer.id == sub.c.col) + .options(selectinload(Answer.users_all)) + ) + .scalars() + .all() ) for answer in answs: asd = answer.to_json() From 3a92bf4c361f0b26b2c2041d5ea597fa0556567e Mon Sep 17 00:00:00 2001 From: dezhidki Date: Fri, 4 Aug 2023 13:59:13 +0300 Subject: [PATCH 26/34] Fix regressions in test after Chromedriver upgrade --- .../csplugin/python_after_answer.png | Bin 9168 -> 9063 bytes .../csplugin/python_after_answer_switch.png | Bin 7456 -> 7333 bytes .../csplugin/python_before_answer.png | Bin 7456 -> 7333 bytes .../pareditor/textarea_hello_world.png | Bin 35196 -> 35068 bytes timApp/tests/browser/test_model_answer.py | 6 ++-- timApp/tests/server/test_csplugin.py | 2 ++ timApp/tests/server/timroutetest.py | 28 ++++++++++++++++++ 7 files changed, 33 insertions(+), 3 deletions(-) diff --git a/timApp/tests/browser/expected_screenshots/csplugin/python_after_answer.png b/timApp/tests/browser/expected_screenshots/csplugin/python_after_answer.png index acb318810608b7679363ba7eecf9bc1c042132d5..b229258320e91c64ae9444c7b26a9b77dd15f823 100644 GIT binary patch literal 9063 zcmc(EcR1T`*l&Jax^OzUp zTB)tJ#3~|II8T1>xvuy8@xFhY>s;p?E|Tx_ea8KK?(sbL=YD6Pr*-Tw?_mf8atwCw zt`P*XR}g$pJ$L~8Uuf)g1RwkC)pgY&kfMkq+mH5x=ZjD78Ra&VRzL{y!z*dyuFT3kL~{Lh|-l4I#|?r>ZEcQNx_4KGRR7d?KC?x`P8+0;}4Nw zuETIQy2@ZV6;ui}tLEJg{<~P;Z=#9}=_fm}dt6Xw5M#Snxj1@i zHB4c+q0J>@f^}UbX@HIO=*dZ&U&i#WdgfE}?N=-*atFT4eC}=A2VO1Zidb~&+Di`k zY{Vp!&a<0F|2~d7A??_EJ^udHWTgLz#$)a`AEnfxtI-ILq{O~Sx}5o5Ky8@DXKQCS8E9et*!z{=uI@bNOn>^}gx>%WD9z)(#ae@09QPvBCK$Up@eUSijV5s!%m}#+S(zg9s-ss zO{W&Z!NY;$eCsa4Hs4~h=+%VwLTt^ z0QD_5E3|f^8v{6&ez<*V&||S!lZ%VH2H3g5s+;KlN>08qq$Ki_7)cxJ)PT)AFtKTcvsH|^g&Av$SwSlY zD6K($Jt$S68<= z5v6~4?km4qfF%&e;O|(2X~x|{WvCwQfpRCx%G&JgEXLivyE9wI&2qQ#JUSPw61#Ek zrrnQ@jtaAGQO(Or$| z*_~^s>yW7S$Zrl(xB}8Z6zjyB~Si00>zQ1~*qpuGUbyc_1v9K2Zih~oi zGSy~Y>5|TU{lUX)CH5T`j-8XftD7KGngF>x7 zeE1L&8fr}MCH3HS&+{v}i|At&6cwAPR5*C0SfGZPcuVUgiNV8^F#*rvLyu`e%20T5WlG*=WAMOxADRg=Af8?%?1Miy0ms?k`4$)@?8A z?BBmX?^$0l!Mu#X$LGvg2O5noDOrJ5j{&FNf8YQ?Ba|DLMe89Ffi(Tqo`u=(qoShb z=I4>p@e^}%&*B$K(Icubqg%H=TQRA{NFBDBb%rhI1DK6}W%Q{H&MYpXXSF9rN7J&h zgec^opnb1{f}Va2I<(AW#?{u=D!I?3!EO9aZ`!xFGDFMl+Iab)J|^HPx}u`O($Z2U zVB0H3!phujy1mT}ilucI1w{hH^YoIHl>F8|Wt|tyvaa>PWjS==^FWXQfF|-Qh3jgl3@Nk)M~TjI$B!RpW0@%Dbb)MWN-!x(@N{$Q>LJ1xt5eq3 zz2fiVw{~>=^vp=+*}zf|_yM$SW=2MIOiZ@(5E+5x+!_m&JahW=%FnGjV_VxS?HJK) z@P~-)8yFyh9w8wiEpE_?;K;)I9dn={bFzD$UaBey;6;gw%+1Yxre5&q+nbh_mSUy4 z?e$~d8yiK%#3(fR(TNEm0fAILO5**iib_huWy-xQAZSZ(&k3-V4f+esk%TroSRV&G zP7E#)$ZL9#V?V&jijCy`ii*EeVh<~vheS*Ip2;RFdrFK9JeEsbRQklS-aEYLelsXZ z#oGvk%Fgw_<7JN<4)ZHw?gAtTh>|I*LdMV<^Hcqc>Ul{hlvOZbLNIB@99teA+yjB6 zJ)E3%-u?dWG+W@--M>u&7z_pnG}^|S5|`*CDJdD>HnnG$QeIeC2-H>PNR=Q?y1KfW zS@kZ5*4NidSXQQic!G$vst>T@;Ji?(Qu`wm7`e!e8-~CVfO?0zYrNf(72Pz?%R5J> zrlx{maVe@!ql8Q=Kw0GZ!~n z#?HNu*5(P#}#O;HsU%jx#yuJk*>XLxw*P4Vil4&btumX`KF-$_VF_^h~SQFFhGw6(Q6)pj>JR4N8QFlldZPf!c2d-?LEt-XDYgcVstNa(xt zg}XR*Jdcb#!9J@I2!xW$=<}<(30XkEmZ2eN#}=ivwUr@1`sYvj+WdectvgRCV7qc` ze4Md1fTlDwTxn7|efqSlQ-A5R5m#@6M~@z%et&vY*A4CzOA5SF4{sG9m(SczkafhD zmdb3bum1p|#E6=9<|9fLh&R{{92y!59Qe%Hvm!8pWmPfwZi2rbzolBa!`dRkqOO8d zI>yh>ue;XQ6WG;v!hJB@glHyyXV^mqOEWnJ!YuFxatN0g9=N!}=;s%_P@mH5*0ym^ zSJz)3E^EnnFWXYK+bW(y8QsRQNN{23EO#2Xz5-6YIQJ*dcfnyM1e0~_HFEO37UXXK$3Se(&3pmnQ%&_No46g>{<* z)%DR=TS{7|h4o7LC=|*1>oAO`XB^lbsyp}5HF?hk%b07%nhZJ>4vwQUCr+dRoF{&p zimIwwU)Bn5U9tGGFbS**gu0*55j?mTKd(M7_3*h}M@q7zVw95KBRTZhXSFWRZFd|8 zMn@lNXlXf)H-?p~;yISv+@_)zg`_{^JB>DA$y5u{wA>{L&0x$|+*cj_-JJhRG~I;{N>KN?kxwTua;jA>949E}T)L}5mMuyJmYQ%be3jB4WX&nM8Tks@~HPlbLGx&A6@kCK(-N%`>ICbbdz!cF8W?l+pVuY2owyUW5t z6#5&t%W2%`kYD)S)kx1n)ICr%I7D1727Pwnsu}FY2Y3^a9WsR(lxyb_R{bkA zYuAJL_}}~9;|ZAatLueVV-EEcSZ3TBur$~@sODjvoM_h0^qJoAGdXfQQ)X*k&L`8& ziL!h_oJi2u3Y6t9elR|@*d6z+pL@b#F9h=XZu(4kPx_<%NRmK)ciCR?uD_A0KGUz? zlsoet?asG5XQe~wZkVvJatm0W|H#W}EyOG+nPhFt^UAGwW@jPIdsCGHH6&~5^^hsM zni4NcOo$_%o0zsoM4v@dt;CZQS?2V?wviKp#!ct?c~0;CY;v`S=e5!io6|grrqWNt zL!c|!)~Vctn1OIgx9gjBVf=%ZzJWf6+drJW)po;@_S%cOtcnsD%>Jg?Pzj1fA)vCJw!Y$hd?b0*jke-8$FAGh z%`sC_%5RO}shX@nkHQ>zwUI2wqnu@vrj|p=Tn0lr&;b+b>nkji3hn0V@BIm*2eR zq>M3JY#)iwaii|4)jOb9_mh%-BIsA4nQa_;^ro<1W{m0bh~JAgF+#cmgW7>XH8?a? zb{0imYx+)na;u>_)?tQz1#ZtlSxoz;EM{64UF~r{(0V}CS5L!=zb9bgaD#Q_B^ai( z0^ZrR8nWtkp5;quO!BvWq+vh-#&Tb>B$KpqYy2Eo!P|uV+PX^=mahZpR6E@@U0~(2~&9 zt=0yjv<|_%+$*J}!xDRMK_Yz9!-S)UXl}$H#CCt(WkyR&m&gRNNCvJ@O8)Nr&;z6n zneaBVws8%0oq8$U{4DRaD40*Ju`a9fhWYF{M66S+^E<=T@SWo7CM-CP_ZJ$3AG!fU z8IYot4w!UPcRhuu%kk6_lky-U`Mp(U=LHER#t?Dy*+zhsZd6 zQ!5#RGB0WM@A&}lY2oflIyqk~uI9C!@uvMZZd?v1uzrronU5^1(F!FT9y;$>mhU}Z zY_^gOwJf`Qg?R#VT>^%%%WH5N;K5=|f`vtCGtEJMeNEq_SPYrQU#jT9m4gvVg%CAK zqN_T9i(vTcvV~`r@XqGt*FuVl{mnr48mWM#sO`ZCgFrpL-of~@YOgWF4t1(>+fe+zU^|XHL9b&Tge<#O$au9+SpxO{0@-K zeR~dRA;67UG8&-q?pW@nY}31Ln>Kzk?Fud}zFr+0o7Z~1WH`Cr_%!!ii&;~AC{+k{ za|D%1Hq}O{Kh07_koAGv0K>D=^&{mObrnwMh1Xy_U0dbStZ{q`u5QVG;#TK?TDp6h zQ5$VJ$1B|2%6CS7+2UHK25k_2Zbx<17sek&i_KnDj&i>)aYKJHsxW(cJvKk1K#!b+ z3n0$pvX4;bo8r8#v=xu!oZp%mIChfutC$e`?499?`S3Dv)G)2-2n2#*``zD?=H>b7 zTe$A3ya-j`{zvX)F4wT;S<8j?@kliMsc!;wP20CJ_k)cCWqVbS)MNJ@BYrf@sBkR+ zCq%t;9Jqa#g*v0V(3tcL8~wM&HHtR#FsI@rjZRA2REl%a1u?bzBibXkG&pqlI3YDh z>9EmBEniq?nQ}_Y6=OO` zdW$Q}wik=?leL=ECL^ZWa0Pg~A8mbq&Eg$rm#?`Ii54yo#cLH@lO=2@KXGlpc>6O` zNctQQh=Cyg>0-O-)R55n$JlQ!EHa{EI^?nEb`MXW4LeBaI1wFxw~EJ}w^vq<0uJmx zHiz7RFPGjorEteZW!9>MavOrM#@@2#RG8{6pg(1oU%&Awj;%O;q5flzlCw1NVafpo z`J4mGNJ+n}~4SO_sczFc;cRV=*rf+qIX?Z#Z+0VVI9kgGv+?d1!= z4t>U>MpnDl#&KDi0oh#K-d|AHwZ>v(G?KIp#(A?zrrIRBd=AZEvHQeEXic^&2 z8HVpN+7q_#R6f=do^9yu0jbKfLW4KpRAMI0^R8en$7~XHn&Wlglkh){Z5pPFkJ&Cy zN_^`l<8f*Mz`VF0RB}{~LdOkSzzwyd3iYgptY3Szo&3 zXT$UZy6Qf9@v9}~F9t&KOHZbI$krPwJ%X^51&vbq&6jHp-z{n{DAcQrw7^5+OmHAPsRzF|N48G?G_2E}!ld`b_MA~FOKe<4Wu<=8Yx#b} z`NBD1cMgMMVT`s8t+Ky`uD8_1i4I@tQ#+aU7mSB?M(BBlVllCK&!DRyhYb(x6Bgo=6Q}KEhwwPq->&@HLKeGIX61o zEck;MR#tYruDq=c{%wF3d_(`*6{s8rUZ8%AJAcg`<;k@xcF{x(Zh0Q> z-UYm$u-nLqQgzR3wR0G6f?h8~*EwKZ+-hZ?gwob_t?LDpNXYS2v!3FH{FZ%X`FIfA ztQSpg+C%`~Qc?JyX)fA~bMY1WrC710w=F+fjes>1&(AP<=6a_o zoHJiL12&2y9P{F}J8C32nHE;9{SlVwr!ckXrs_ zx{e+@d3W5bi?JpDK)mwfmD+!{4H`Z@fZrQIfQJ zEJ41w7To#&ol_GnVdN?=>@S_0O;+7lPXYKGzD#}sb8lkjd53?`4;3Ar|Br+x;_82$ z3hw8=U8BPlRlLl6V!!{A*d3%ccNta;jQ)>|OdXnV&mr!J=ZWT)$W95O4OR0$Gpxm+ z_rA4Z1@_2UV$36@(*kJvc&lNbQNx+RlGj&P(e`vaJJX5?+rxVB$M?(psP`xQuAZk zl$z5(d0W?7LkO3#yL+i#l0rIQK5Q*Uik@fzYJ$x~1FpM>p;#^Xr18BW=mJP1*@vH> z`^I8jTnqrk21uR!SdM-BL=+VffGsRn1y%$ZP3H;`76hE!GK0~x(xH|*z6r=;Hfz*{ z*Db*suY9+aM(nV@a0sy566MhWqnn+boooiRwe`M*056zh>F3cSf*nw15~wz%*ZMM1 zpeh9_Apvzg{D5@<(>?7-f%ca<#vM4u8=W)H=9Y)5J?$+PXfy(#p8>z54XQH&Ef{*E_^UAlCeU685|U}3ID z0cvn{b=6PV2FUYV{M@PADPCiT`~i40YGEp`+_PN3Y&n1$2gbuAV1^7TWCT$3paa&Z z`?S7FdRp2=HZRSu?9qxHc>DICX`YeB+S(f6-A&@9k$|m2bsPBk)vyuuT6$RKJY){& zJF{E2o`>@(W&k#<-$)CT001GMbMmt0%*J#o9B^AQE~B4S%4)5uJ!oAy5$0v6Y~fTt zBWr7G#aJDEeYm-K!pS%`uOQ;G!F@YH9$f&+SCk1F4TWHSJt8?nwF&I`^DX!a%#m7P zYGi~nuW(M5+|q@?evLJRUV#-A6|vc9P!CC>mxBjDd&WdWL|A)Ia`7v;+yb;$9N=Gp ze3xKs_PuVCCT3OVhrq4&*{9~YjyJ}B#zjX9>FevWOIM&qgUia#&sQm93GA*12e26M zWVJwRoD6!nhL4h+o&8sc?JE)Q{m#$wadYGA{I|@fn2RImjf$L{9H819P&P!wR#jEy zndBL9*4hB})vD6v3km^HV=tdG4`}xZK-+)*e5)-<(O6%NRSF7a?yEDu_~=0FxV~p* zpp1iE7*vc0ym;qut?y#>{1xqJgU63Ekg9@$f=!rz1JmbBp^@A7e0<2DoMRtf@9ic5 z0|YhONmi#4(NkOhBtd=UEIwXC6laZ%jV+h@gPIYFT0p`~wy>#6^U|juK>Hb9-h?Nt zD>|{!&9*IOfhZKszXA$K?8%mxlGYJFGA1XKM^g0S#fu{y{9xY?kGy$*2vLQ}-m`bF zX{`?yW(2&D>(r@B`uZKg1-_v2hH-T@G%$!1$}`H=_5p?IQc%88aS}LrGM&fTOC6M@ zK&23FXNL!(IL(HaVJ5Zrb8t{V0T(Cp?6(vPaJyvp+0Hu^{-7>}8mX(TsjUUIoHKlU zNlz3xIXTOo{u=uu$yR~E!L!SM-T^ArapK1Z^G^?702HHv@pq{04tBDhZmrFOG6U|_ zt5+&eC>KA!IE+`?_H9N+Mri=c2PQvkJ_(ErF;eOJ`QSBEmUTjXr*_1y0b;)UnYzpE_f;2J&Q1toMf5q73FS|l% zn*ZR&WeFbku08|0wayBJTm){i{{DadybdI@RO_V3P|q_V5nSwp$;0NWJz=p1tk0hb>oHn(Yz6fByNW-1#(kdaiYLZx_4A z|0mk?e~YZGmQE>9@|`7+2G|mhp2SX#nR>^$l(h~(hbnOyk54-d0lpEqf9~6;Lq6o9 z2;{JHE9D=WvTS$nJbUj;Z)rCfJYn>qAJXV)i{U%F%t!Ez)r7*4wT9VN4YJ+8P$}3; zSoffPpP2rg+}OmLdz!9E*dgpedBqMEqL`fZ?#Q=NiMgzHYqA|(4iHz6_46wde+Zu& zRX0b5!}9mM6=DS*QWrV$d@z^`yuISC@xa~okvmf92?hy1AktFOH^rqC#HD0RZroJ5 zaYN~*jF^;^l9W^u^f@3C{+9txXj=z6@Be6&&2$(HM6Jv@=BJhODCO U`8Fd-@D2jg(7Ri3`{B#~0acz!QUCw| literal 9168 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ͅ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 diff --git a/timApp/tests/browser/expected_screenshots/csplugin/python_after_answer_switch.png b/timApp/tests/browser/expected_screenshots/csplugin/python_after_answer_switch.png index a806d2a598646b4e1d49bfea402a612138ed8cae..1d51c0681468b36424839339f676bac6a264fee1 100644 GIT binary patch literal 7333 zcmc(EWmuHax9^J}ARQ9Y-6=gINJ&XcDGh>zbf*YIw}60jOM}3Ov_ndFcY~yK-xvPp z%l&ZAr~90<=XvIhz4zLC|JGW+wbo3isuPu1 z;05J_gpvdRR7PW78$Sl0X-wtRl>oq#2>|?H0B{Mu^4kOe7ajoEHUa=h5&)3cr`D;x z06S1kUdu`YclV#n#)3HT4VtsO(krwLbQEk%#>w*55da{Ml$Vy!aG&0t^U!#zJ&k%$ z@EMgX=GA*bqF2%yc-Xbl&lqh@#RFpGJH9(SjvPw*uD0=}1Fy49lu1oCq66C@^r{wX z2+wWh5&mN$JZTA(r|%2i);GV5A102G@iIQXNE?wY+8%G*FSQyd{P}mDEmn^!5m{XF z9aqyFexzZ}a7=>kBf9&RooHri4EsIzj@BK9mb%kN1P@(x{B?bqMseQvGEB+vRJNYj zyu!w+WH%`^1ZN5Qv;J}owpRtpX*@R5{5I57 z_s|LXYkk49v$HEPY^E|d=YC1D7X9*8d>~!)FQ@j~x1md|!KSNSvFPaN-zys}NAfEw zD)4SmX*iHoxb@<`e0h4xCnO{*Cnp!M@>L-(Mj>x=veGooEQ%6}TBs4GB6hJlhT}~C zl!5{aeB7P+S)AKcUCkdNO}8^uRWQDLl4c0R$T3XTSd)yG>ISz4<62)m0MU4QTs`zX zJRDO`PtTw*Dx8FyP1E*hy-!X-0dcs}X+39aY)q>cFLqZBAtfVwCdq;K)dT4u_V!4=O4ukWN*#!RqA;_TrP0hD=p`XzlDogFq+>{=x)NxY^; zW@eh!cNz&WxjgmWR1w0cs3;Hvdq+n^cLMvE>$1AK`r`gRE*~Eszs>YhpUdOl7e^Zj z^UVzn6eg9VUT1ryE>~@#guLztM#dg}-vsPkU5PIRy-q)Z@&bM0FK}4s+1WAo8_%eE zdwY%S?4lW8e;rMJs=-V$aDLj( zNh&I0W#{BLobBl=rV6WNrKY5iM^T9Q|A}K>DK2k9%+Sc zH-{iIeEj@v*{{NRY-ferIy#n*w?@!$aXaBQGqC5MZZDYgmD9rZ=ISw9Q*zY{?Gv-6 zL3_Zbrp7fiT!73R9UngpvV5zfW7M0(TLFjL9(+!H;qjb~o*tB~_LPW-mY@IW(Z+zR zj!sgy$~!YNMsFVH2ctc+8p zU$137b-4%QU%iG~jbW6Pl?gkphI9`+yZ2O4O*;a0QIGTb{ic9~1R{vbHtZRn`QpJ+ zD+@b2VfWPZ^m1=9-}zQS1|A+B0EI$@ArMKG{jCu0N*ydIsa6u4cKlH3gTq6K{rN^D zV0XGEpuQejruU03erRGMxU7sznhqZe8#{2|$i&110H~;_T;LmPYk^xM1<39%t}Q`l zuo%u=4=2~x(}QJ6g9hIk#0v4^K8#sc|0qpIWPp*DeVu7b&po>!X2+nws|cMlXlETc6eSbqqI2 z0zyJ-21osR*FZ(4H=7x3O~_|^a{=J}InKQi{r;Ub{*Nve-K4U%e@O@140K%6@3i;o zFC2f}*Obt+23RSVfq4G1^0$J;`Onl?qk(R}bo4P|EZR@hZI1#BLkMPe_Sb8bU2Fd! z%7r zyBdQ2&umeg5344sYhStT&3;y7BK!(kJhqRI&*b#96{o!!7reBjgeI|P?6wCU7f0A; z!po&!ms=qwDynbX^{uG3wssMua=F*93CA@xHE9I}wQ9Yxu)xiN8Z;isS2{R4k}5Z7 zjERl?9LKDJn3{S9(k5Q0G)OKT8J}(oE9^j0I6gU9+Sw@?Zkn2zL3Y2UEY?z0^&1@> zg`CMND0nW>N0ZZmgvsdE|3E6X3XHUYfq^;=?!4VSJr4WxqSptlxZpy?P87a2MMXub z@7{$C536fvXaGQWcQ^QWsZO#Dhr_{mBJS+W|KrCGJQ9-N<74N*=lXTdNPx7gZ1D+= z0t12f$3SC*En{s95885z+7Vpob=wP~rTMj-2R>cVMgXaEQVLi+qY zI!JG@3mSmOXt)pjnbK{P2TiDk|JG>patBqt}QM?mD|u@MU;!1osA6xG&oIp{rlv>E?Yj1X-rq9kRkWO zKsrbr0RaKXsi~=hMVjnj8X(A$1bV6@T`VZ31&mn0V4aJ#90S4R3iMr_Gb@uydIT6UTSrHq(#uA*F=u%D|8m;E zFe|#^j+L`t>A>bNXwcCq^9>u%ln8*iWMyRuf4Xg`pm_QeW$_T@PAV7&%xauB{fIf= zEe_|s7WTPx4%k!P+TP9vyY7K(_PqfBkQwBZlr(E|o?DHwfD%dhKJ>$W{Suk~bmwzV zKMUSRN&!(rh(OH1rU+8rM}U!u$)iwd|I^(W%r!xqX%upD^4j|P91y*Og9CBU4Xr8J z64KHl-&UEk-e^>i$SWzm($ky8%*cO8yIfNEEeXJ!LEBCQAGPFbPqd1fp2(A!ms3Ju z9F~`t7xws<3%@#67qp!X0n-**M#j8;w?I6kmUz}T_WxGp!2l)L?IC}!gUGi&PeLpf z$uu@7%Wo3KH-~bTR##z*i$*rVl94)kdK7{mCl3%L^aP*_s_n)%BCGG#G*1tYjI{Qr zLGh`mqI|ASEuj=vuv~T4AYAGwP@~3OF|^Y-!4Iis_tBG(=T4H_KH;-L*yPo7<=X4Z z%l_HfbT+{fk@3=WYJXA*Kw``mQnCRDx=9;cL22)|C7tWZo#F>5O5QtD$R^{94Eu(E zsd&HsT@l`M0lonGKI|b^ik0Dg0$L{6B25S+AEuJEGr%6E@rTKyKa93~8i)5VW|)QP zzr?HkmuR`1r2k1l3My#{+|!<9#jb|pGOnNDS8v(t&GVBkf2Jov^P}NX4@dDd{m%L1 z!nMcJyYYgf+a&@ZgxZ`{X+2{kI`=$!+qLG{OFpALv-F=_5r0&W!g?$nGnx>OvJ!rE z?mr){R%Qy*^y0FB-a`vw8#MhTrgz7`Bh3$8Ry?94RnkUQ;vT8vj>R_T{|1*BnPgmD z+@Tf$4C)oyzl&lH%(pigI$iG_ZGv@*jSVSZCCWps=UfH3kh4Eq+yAqI07fUDbvAEK z7O;z&+YkEC=Z}}s82B;IpJBGpZL7YjcqZXA&4!&1t zSUZ5Kid`=}5v7%`@mMJ4%*Sye{E8XYb7-x>mR|yilnpdPr`jqUav!INXkUY&O}?ma zR@n9`2p7E-Rceg(*3u@Y<_Ph#iykMv(k4lm5cT}y0Fz}}Zf#OddNE3lYND^F(1q9z zsGRg+dsfhN6m0W`zJ^K&Jiir15-g^prs8?f&-zA0-I-^NL{{~qa93&5{)Gj99Tg{` zt!z6{RZE|=cS5fU#+Am%s_LdgnofWw(RalS^GeP0ZSXGXk}Cu`p%3)Q-&3tg_|g021UM-|}V)aZv;f=sbDryu<=eWU7mOt=(BAV^mAa;Io_P%gmJwL(Jv zEu$kwI!@n|$1-u9(#2yYsfpjG0g*{fryRLv35e5bv{&Z`hWHg1M)V9~S)0PfP+y3n ze%3t6*pjHzOTinmXwJ?RTC!SH-ak|2v(X#D3YuDf4yLw8DzGMjkuzV!`C6>7yVzq; zT?aHOZ1E#wL%;nidMa*==&^jp5g+zw19!QtR(zFtgHWi0V(nPI-T4rHy>Wc}JB2HA z1-6TE)%YD7vN`SN9v_g?3k&S{nQI8x8W6gdU~jr ziVJA;3#v(2`oX@2R4r?;fH~EdBia}*kDQdcvC+;r*nt$D*A*f}) zFa9QD*yt%(06$Zqen$CxlR>n4ff}kH{z0{)1HvfV-~EcoWfz8BAl_DtrD> z=Y#j<3M)mp!keO~08Hs9m{t z;KG69O`bU69kTwlBk5#B1n`TJV~C$|`A1B({@P(RC2La54X=Npt4&*%i_K-nzX|%8 zT^`rL{xdfnabxhv#GdKIABMoozt$`BQI&w7=9|k!*U3&cX!%9^TJ5*g$j?-qe>q0R z{$R)ic_Pk?T6O{|?9BZ!i^WDLa@bFH~bkTzm31->z?dN{aIEv=3@ zJG=YRTHLFaxVUR@V6V>Nm)E*X4Z%?FO*N1bTw^eM7pTTdw z`nd#`o6Z_S#kDsM1CN%|hBjw@i4ZHflPjN^yZ@|5CPq7zbbp_1&r6o_CE)iQ#b9Zp zz_g%H>%H8wn=5-e>p~4m^N!2Gk+IL8bx8Vpu>#@Ngl-%79M?jl=MB)aLCmZAHJukh zC$<4J8Ojz5!xWgEC(`VHnFP9%pZzr*8x=dShfDft>tgLrk&)P$k9obQa(s>FaKW`6 zdC(>v{dIH4B6QaYjTUM+ohitngpC9+5%7K!B^}F6AHH zGOg!dy=!{ddRxrGRa(-_J6ubRtJfNhR>oSwCpapVsUlmFwE7r``Li0L;O9cYB_ALz5AO@%P3HN(Z`Epi3iUij_k*g?!1s`j%l;%Tw&VW9Av(l@Kq1Dd;c` zd;;2xHVqg7V@Nyn1`3%rDgeza||tP}y>I8Mpwc zP%U!PwC599uKCZ+iS%U)9!g*x<}l|)%Y$1Hkr{we-= zLcC_SI#Va;Sth^XuLZWdx>%8blABp_TN{o_&gV$%XC0ZLv7~Xqb;e7zR4kn3>P0wa z$#QK1Orywt^O#&^myjSSHP;Aq;fv zL42g2!$?z&%T=gQLlT8QX}FGIyS zoT<7=#Wl^K4QMpW#KCgk6Sr9Q3Icv0M_mi&;Uee>6Xx%x940>;nYtb)+jQ$ZX!30=5_R3P(7Q&6cm){B+ydtFMt%JNZ?m2YtCEsGD zi2JZBl7@v`$raQzM`qrE%EH20u9lx!OVP&}8E7Wrw>*qNN*A0_Olq} z;aMO)3rAtgJ%z3>yqyUS0CTHBxeXY_FL#oysaF^zSelxSFH*FV#LkMjZ}zuAA`tT| zpv*59rPmYwyBV^$^f3^c9B6N8M(a(n+ndQf9{7q7d6gs)^dx+ugy_!e868sN@nHQ! zS6SV*{8biBSpktzC!rQqLlVwXMSE5Whpv%ZX16_!th7cEfTdbZNB#4{ZNr)RN#Tl) z;-_Xzb?T?a5|e9WGxvrU%C55b@>4DVyhtm>PdHFD=8`o~d$v5{I8@qLgJ>!!nhp=FDux-hZ$K- zTnv|W!Y(IbCSR%oSCxrmPTgb{>nRU+qbwS(J_T2RTzZ-=-!YrPojI}LHQ1CV@>OLU z8#q4})sBzXG(v>QG0gW!eN@1a@%F;Xm$@Y6JJ|BZ5v9Zjf$5m4#R7X<3tpys4r^T% zm^r)fn8#p#-Mb`2GO#hW>ZRreZGtR7CIyz-92@#(Vvqj4vm4NYdsV*dQw|T&+Ya`i z7roEpq`h1EtaEyw72$?)&eV@1SgB0m>mB6K|Ef?09 z`uNKrFQc<92CO6(y!JdI?CGCke?K$?q#dD3yfX+Lxbs0Z{dybJO7e!s4#rHEh!Zds zhmvLE#7AMc91Lo+R%JHSVB}Pi3r)G~fP`v6&&I)s+q-EUYu4X&@#Cz|4yFf5lxseQ z*`tQ(d;U8AO{pIMwMTW6fNzZflsdQl|A(s=GxY}u?$I~yeKGucOyQQ5WC z1j>DV6&_lj&QC2$clWF#Hwv0tdUdGTnZ1lRXYBSIXuz~QLYM+`(VpX>LJ{zC71qj0 zOHu}x0{?bk{%`8ILe3|ZHs%;z8dpE~YGO27I(~%0b3l0@+V_Q3^3DP!3;V~}(2$Zx zEbMKttn}%qhk9`{G7EkN>!se0WJ|{Siz`D*57nKyDM`@hdxL_rANg3~rkWB1%tY~r z$fZiT*{>lL)J-_6kHzj^;fuD1Zar)*_kXFf{QqC;#z#d4;y8Eb|jnaCf9<01p z8#|jpOdZX@3-FxhIUgsFFeeYc1}`6kmlwh(#LmM5;o&JA`|@7T^`&6X9X`jt~I*0QpzS(q)oH{{IKk5^3Q8 literal 7456 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 diff --git a/timApp/tests/browser/expected_screenshots/csplugin/python_before_answer.png b/timApp/tests/browser/expected_screenshots/csplugin/python_before_answer.png index 86ea5b561968a188ec22344bc85420b1363da59c..1f538710d1b76870dcfc52ffc084a28ae5bb7eca 100644 GIT binary patch literal 7333 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 literal 7456 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 diff --git a/timApp/tests/browser/expected_screenshots/pareditor/textarea_hello_world.png b/timApp/tests/browser/expected_screenshots/pareditor/textarea_hello_world.png index 3044e02144374b09fe714c48f9288bf935a30761..ceba1baa069967d433f805d28a16db53fb47c535 100644 GIT binary patch literal 35068 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 HyZhe&zg&R_ literal 35196 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 diff --git a/timApp/tests/browser/test_model_answer.py b/timApp/tests/browser/test_model_answer.py index f26164e232..1be85409ab 100644 --- a/timApp/tests/browser/test_model_answer.py +++ b/timApp/tests/browser/test_model_answer.py @@ -49,7 +49,7 @@ def test_generic_model_answer(self): self.assertEqual("Hello", model_answer.text) self.test_user_2.remove_access(d.id, "view") db.session.commit() - db.session.refresh(Block.query.get(d.block.id)) + db.session.refresh(db.session.get(Block, d.block.id)) self.get(f"/getModelAnswer/{d.id}.lock", expect_status=403) def test_model_answer_formatting(self): @@ -176,7 +176,7 @@ def test_model_answer_points(self): ) self.test_user_2.grant_access(d, AccessType.view) db.session.commit() - db.session.refresh(Block.query.get(d.block.id)) + db.session.refresh(db.session.get(Block, d.block.id)) self.login_test2() error_msg = "points from this task to view the model answer" self.get( @@ -284,7 +284,7 @@ def test_model_answer_hidepoints(self): ) self.test_user_2.grant_access(d, AccessType.view) db.session.commit() - db.session.refresh(Block.query.get(d.block.id)) + db.session.refresh(db.session.get(Block, d.block.id)) self.login_test2() self.post_answer("csPlugin", f"{d.id}.cs", user_input={"usercode": "1"}) self.post_answer("qst", f"{d.id}.qst", user_input={"answers": [["1"]]}) diff --git a/timApp/tests/server/test_csplugin.py b/timApp/tests/server/test_csplugin.py index 87b3e26e87..9a925557e3 100644 --- a/timApp/tests/server/test_csplugin.py +++ b/timApp/tests/server/test_csplugin.py @@ -4,6 +4,7 @@ class CsPluginTest(TimRouteTest): def test_csplugin_pointsrule(self): + self.wait_for_url("http://csplugin:5000/cs/reqs") self.login_test1() d = self.create_doc( initial_par=""" @@ -62,6 +63,7 @@ def first_answer(): self.assertEqual(2, first_answer().points) def test_csplugin_csharp(self): + self.wait_for_url("http://csplugin:5000/cs/reqs") self.login_test1() d = self.create_doc( initial_par=""" diff --git a/timApp/tests/server/timroutetest.py b/timApp/tests/server/timroutetest.py index 1d22b8d72b..98fc9b346b 100644 --- a/timApp/tests/server/timroutetest.py +++ b/timApp/tests/server/timroutetest.py @@ -4,6 +4,7 @@ import json import re import socket +import time import warnings from base64 import b64encode from contextlib import contextmanager @@ -11,6 +12,7 @@ from functools import lru_cache from typing import Union, Any, Generator +import requests import responses from flask import ( Response, @@ -1397,6 +1399,32 @@ def temp_config(self, settings: dict[str, Any]): for k, v in old_settings.items(): app.config[k] = v + @staticmethod + def wait_for_url(url: str, wait_time: float = 1.0, timeout: float = 30.0) -> None: + """ + Waits for a URL to become available. Useful for waiting for a container to start. + + :param url: URL to wait for + :param wait_time: Time to wait between requests + :param timeout: Timeout in seconds. If None, uses default timeout. + """ + start_time = time.time() + while True: + # noinspection PyBroadException + try: + res = requests.get(url) + ok = res.status_code == 200 + except Exception: + ok = False + + if not ok: + now = time.time() + if now - start_time > timeout: + raise TimeoutError(f"Timeout waiting for {url}") + time.sleep(wait_time) + else: + break + class TimRouteTest(TimRouteTestBase): def _init_client(self) -> FlaskClient: From 71a6e19944ede78534386a32827c3bbb354c9d48 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Fri, 4 Aug 2023 15:27:37 +0300 Subject: [PATCH 27/34] Fix answer points roundup The roundup seems to be introduced by Python 3.11. Now, all answers are automatically rounded to remove potential floating point round-off error. --- timApp/answer/answers.py | 3 +++ timApp/tests/server/test_csplugin.py | 2 ++ tim_common/utils.py | 31 +++++++++++++++++++++++++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/timApp/answer/answers.py b/timApp/answer/answers.py index 4e875c481a..af5ff4d7d9 100644 --- a/timApp/answer/answers.py +++ b/timApp/answer/answers.py @@ -52,6 +52,7 @@ ) from timApp.util.logger import log_warning from timApp.velp.annotation_model import Annotation +from tim_common.utils import round_float_error @dataclass @@ -161,6 +162,8 @@ def save_answer( :param answered_on: If specified, overrides the date when the answer was saved. If None, uses the current date. """ + # Never save points with round-off errors (could be caused by any computation) + points = round_float_error(points) content_str = json.dumps(content) content_len = len(content_str) if max_content_len and content_len > max_content_len: diff --git a/timApp/tests/server/test_csplugin.py b/timApp/tests/server/test_csplugin.py index 9a925557e3..8a6321cd6f 100644 --- a/timApp/tests/server/test_csplugin.py +++ b/timApp/tests/server/test_csplugin.py @@ -4,6 +4,7 @@ class CsPluginTest(TimRouteTest): def test_csplugin_pointsrule(self): + # Ensure csplugin is fully available since it's used in the plugin self.wait_for_url("http://csplugin:5000/cs/reqs") self.login_test1() d = self.create_doc( @@ -63,6 +64,7 @@ def first_answer(): self.assertEqual(2, first_answer().points) def test_csplugin_csharp(self): + # Ensure csplugin is fully available since it's used in the plugin self.wait_for_url("http://csplugin:5000/cs/reqs") self.login_test1() d = self.create_doc( diff --git a/tim_common/utils.py b/tim_common/utils.py index 9e6b6bc8cc..f2f0c1d9a0 100644 --- a/tim_common/utils.py +++ b/tim_common/utils.py @@ -1,5 +1,6 @@ +import sys from dataclasses import field -from typing import Any, Mapping +from typing import Any, Mapping, overload import marshmallow from isodate import Duration, duration_isoformat, parse_duration @@ -63,6 +64,34 @@ def safe_parse_item_list(item_list: str) -> list[str]: return result +@overload +def round_float_error(v: None) -> None: + ... + + +@overload +def round_float_error(v: float) -> float: + ... + + +def round_float_error(v: float | None) -> float | None: + """ + Round floats to remove any round-off errors. + + Example: + >>> round_float_error(0.1 + 0.2) + >>> 0.3 + >>> round_float_error(1.8 + 0.1) + >>> 1.9 + + :param v: Value to round + :return: Rounded value + """ + if v is None: + return None + return round(v, sys.float_info.dig) + + class DurationField(marshmallow.fields.Field): def _serialize( self, value: Duration, attr: str | None, obj: Any, **kwargs: dict[str, Any] From df73f713e77a79161b334db6703e4d08b7e4481e Mon Sep 17 00:00:00 2001 From: dezhidki Date: Mon, 7 Aug 2023 12:08:39 +0300 Subject: [PATCH 28/34] Don't hash ExportedAnswer --- timApp/answer/routes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/timApp/answer/routes.py b/timApp/answer/routes.py index bf75d38b42..638344ccfe 100644 --- a/timApp/answer/routes.py +++ b/timApp/answer/routes.py @@ -1640,14 +1640,14 @@ def import_answers( for d in docs[1:]: filter_cond |= Answer.task_id.startswith(f"{d.id}.") - no_identifier_answers = { + no_identifier_answers = any( a for a in exported_answers if not a.email and not a.username - } + ) if no_identifier_answers: raise RouteException( f"Some answer don't have email nor username specified, cannot import." ) - mixed_answers = {a for a in exported_answers if a.email and a.username} + mixed_answers = any(a for a in exported_answers if a.email and a.username) if mixed_answers: raise RouteException( "Answers with both email and username are not allowed. " From ac00b928131388fa7aefdb6b46c62255457957d0 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Wed, 9 Aug 2023 13:30:26 +0300 Subject: [PATCH 29/34] cli: Allow running tests with coverage --- cli/commands/test.py | 10 +++++++++- timApp/Dockerfile | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cli/commands/test.py b/cli/commands/test.py index 9362f545b3..82f5b7b009 100644 --- a/cli/commands/test.py +++ b/cli/commands/test.py @@ -15,6 +15,7 @@ class Arguments: down: bool up: bool new_screenshots: bool + coverage: bool BROWSER_TEST_SCRIPT = """ @@ -72,7 +73,8 @@ def run(args: Arguments) -> None: else: test_parameters = ["discover", "-v", f"tests/{args.target}", "test_*.py", "."] - test_command = ["python3", "-m", "unittest", *test_parameters] + base_command = ["python3"] if not args.coverage else ["coverage", "run"] + test_command = [*base_command, "-m", "unittest", *test_parameters] # Browser tests can be flaky (in part of lackluster flask support for Selenium, in part of the tests). # It's better to retry it a few times @@ -125,3 +127,9 @@ def init(parser: ArgumentParser) -> None: "Run a specific test module or test function. Format is [.]. " "Special value 'all' runs all tests.", ) + parser.add_argument( + "--coverage", + help="Run tests with coverage", + action="store_true", + dest="coverage", + ) diff --git a/timApp/Dockerfile b/timApp/Dockerfile index 5a4e2c40b3..999a781193 100755 --- a/timApp/Dockerfile +++ b/timApp/Dockerfile @@ -214,6 +214,7 @@ RUN if [ ! -z "$CHROME_DRIVER_VERSION" ]; \ && ln -fs /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION /usr/bin/chromedriver RUN bash -c "${APT_INSTALL} ripgrep && ${APT_CLEANUP}" +RUN bash -c "${PIP_INSTALL} coverage && ${APT_CLEANUP}" WORKDIR /service From f803bdcf14c5477055431ff53f6fdc37186f464b Mon Sep 17 00:00:00 2001 From: dezhidki Date: Thu, 10 Aug 2023 10:40:25 +0300 Subject: [PATCH 30/34] Fix regressions after SQLAlchemy upgrade --- timApp/admin/language_cli.py | 2 +- timApp/admin/translationservice_cli.py | 2 +- timApp/celery_sqlalchemy_scheduler/schedulers.py | 2 ++ timApp/document/docentry.py | 4 +++- timApp/document/editing/routes.py | 7 +++++-- timApp/document/translation/routes.py | 16 +++++++++------- timApp/folder/folder.py | 12 ++++++------ timApp/item/manage.py | 4 +++- .../messaging/messagelist/messagelist_models.py | 6 +++--- timApp/messaging/timMessage/routes.py | 12 ++++++------ timApp/user/user.py | 2 +- 11 files changed, 40 insertions(+), 29 deletions(-) diff --git a/timApp/admin/language_cli.py b/timApp/admin/language_cli.py index 06b00ba4d0..ff3173937f 100644 --- a/timApp/admin/language_cli.py +++ b/timApp/admin/language_cli.py @@ -103,7 +103,7 @@ def add_all_supported_languages(log: bool = False) -> None: :return: None. """ # Add to the database the languages found in config and skip existing ones. - langset = {x[0] for x in run_sql(select(Language.lang_code)).scalars()} + langset = {x for x in run_sql(select(Language.lang_code)).scalars()} for l in app.config["LANGUAGES"]: if type(l) is dict: lang = Language( diff --git a/timApp/admin/translationservice_cli.py b/timApp/admin/translationservice_cli.py index 254c579a63..c627afb630 100644 --- a/timApp/admin/translationservice_cli.py +++ b/timApp/admin/translationservice_cli.py @@ -52,7 +52,7 @@ def add_all_tr_services_to_session(log: bool = False) -> None: :return: None. """ existing_services = { - x[0] for x in run_sql(select(TranslationService.service_name)).scalars() + x for x in run_sql(select(TranslationService.service_name)).scalars() } for translator, init_data in app.config["MACHINE_TRANSLATORS"]: service_name = translator.__mapper_args__["polymorphic_identity"] diff --git a/timApp/celery_sqlalchemy_scheduler/schedulers.py b/timApp/celery_sqlalchemy_scheduler/schedulers.py index e2a0ce38ca..891a3a8ac9 100644 --- a/timApp/celery_sqlalchemy_scheduler/schedulers.py +++ b/timApp/celery_sqlalchemy_scheduler/schedulers.py @@ -193,6 +193,8 @@ def save(self, fields=tuple()): # Object may not be synchronized, so only # change the fields we care about. obj = session.get(PeriodicTask, self.model.id) + if not obj: + return for field in self.save_fields: setattr(obj, field, getattr(self.model, field)) diff --git a/timApp/document/docentry.py b/timApp/document/docentry.py index d970a2dad6..3d372301c6 100644 --- a/timApp/document/docentry.py +++ b/timApp/document/docentry.py @@ -142,10 +142,12 @@ def find_by_path( # This is a simple way to allow mapping /en to newer /en-US or /en-GB. tr = ( run_sql( - select(Translation).filter( + select(Translation) + .filter( (Translation.src_docid == entry.id) & (Translation.lang_id.like(f"{lang}%")) ) + .limit(1) ) .scalars() .first() diff --git a/timApp/document/editing/routes.py b/timApp/document/editing/routes.py index 3e84a497a1..9de40d022d 100644 --- a/timApp/document/editing/routes.py +++ b/timApp/document/editing/routes.py @@ -56,7 +56,6 @@ from timApp.plugin.qst.qst import question_convert_js_to_yaml from timApp.plugin.save_plugin import save_plugin from timApp.readmark.readings import mark_read - # from timApp.timdb.dbaccess import get_timdb from timApp.timdb.exceptions import TimDbException from timApp.timdb.sqa import db, run_sql @@ -765,7 +764,11 @@ def check_duplicates(pars, doc): duplicate.append(par.get_id()) task_id_to_check = str(doc.doc_id) + "." + task_id if ( - run_sql(select(Answer).filter_by(task_id=task_id_to_check)) + run_sql( + select(Answer) + .filter_by(task_id=task_id_to_check) + .limit(1) + ) .scalars() .first() ): diff --git a/timApp/document/translation/routes.py b/timApp/document/translation/routes.py index e73dea8f58..d6fd32a63d 100644 --- a/timApp/document/translation/routes.py +++ b/timApp/document/translation/routes.py @@ -452,9 +452,9 @@ def add_api_key() -> Response: tr = ( run_sql( - select(TranslationService).filter( - translator == TranslationService.service_name - ) + select(TranslationService) + .filter(translator == TranslationService.service_name) + .limit(1) ) .scalars() .first() @@ -535,9 +535,9 @@ def get_quota(): # TODO Maybe change to use id instead? tr = ( run_sql( - select(TranslationService).filter( - translator == TranslationService.service_name - ) + select(TranslationService) + .filter(translator == TranslationService.service_name) + .limit(1) ) .scalars() .first() @@ -564,9 +564,11 @@ def get_valid_status() -> Response: # Get the translation service by the provided service name. tr = ( run_sql( - select(TranslationService).filter( + select(TranslationService) + .filter( translator == TranslationService.service_name, ) + .limit(1) ) .scalars() .first() diff --git a/timApp/folder/folder.py b/timApp/folder/folder.py index 0187a9a370..07dee248a4 100644 --- a/timApp/folder/folder.py +++ b/timApp/folder/folder.py @@ -53,7 +53,7 @@ def get_by_id(fid) -> Folder | None: @staticmethod def find_by_location(location, name) -> Folder | None: return ( - run_sql(select(Folder).filter_by(name=name, location=location)) + run_sql(select(Folder).filter_by(name=name, location=location).limit(1)) .scalars() .first() ) @@ -178,7 +178,7 @@ def rename_content(self, old_path: str, new_path: str): @property def is_empty(self): stmt = select(Folder.id).filter_by(location=self.path) - if run_sql(stmt.limit()).first(): + if run_sql(stmt.limit(1)).first(): return False stmt = select(DocEntry.id).filter(DocEntry.name.like(self.path + "/%")) return not run_sql(stmt.limit(1)).first() @@ -223,9 +223,9 @@ def get_document( ) -> None | DocEntry: doc = ( run_sql( - select(DocEntry).filter_by( - name=join_location(self.get_full_path(), relative_path) - ) + select(DocEntry) + .filter_by(name=join_location(self.get_full_path(), relative_path)) + .limit(1) ) .scalars() .first() @@ -309,7 +309,7 @@ def create( rel_path, rel_name = split_location(path) folder = ( - run_sql(select(Folder).filter_by(name=rel_name, location=rel_path)) + run_sql(select(Folder).filter_by(name=rel_name, location=rel_path).limit(1)) .scalars() .first() ) diff --git a/timApp/item/manage.py b/timApp/item/manage.py index a4544b8804..6b27d95494 100644 --- a/timApp/item/manage.py +++ b/timApp/item/manage.py @@ -427,11 +427,13 @@ def expire_doc_velp_groups_perms(doc_id: int, ug: UserGroup) -> None: # TODO Should this apply to ALL permissions, instead of just 'view'? acc: BlockAccess | None = ( run_sql( - select(BlockAccess).filter_by( + select(BlockAccess) + .filter_by( type=AccessType.view.value, block_id=vg.id, usergroup_id=ug.id, ) + .limit(1) ) .scalars() .first() diff --git a/timApp/messaging/messagelist/messagelist_models.py b/timApp/messaging/messagelist/messagelist_models.py index 26653fbecf..19f04bdbf1 100644 --- a/timApp/messaging/messagelist/messagelist_models.py +++ b/timApp/messaging/messagelist/messagelist_models.py @@ -15,13 +15,13 @@ Distribution, MessageVerificationType, ) -from timApp.timdb.sqa import db, run_sql +from timApp.timdb.sqa import run_sql from timApp.timdb.types import datetime_tz, DbModel +from timApp.user.usergroup import UserGroup from timApp.util.utils import get_current_time if TYPE_CHECKING: from timApp.item.block import Block - from timApp.user.usergroup import UserGroup class MemberJoinMethod(Enum): @@ -339,7 +339,7 @@ def is_personal_user(self) -> bool: return self.is_external_member() ug = ( - db.session.execute(select(UserGroup).filter_by(id=self.tim_member.group_id)) + run_sql(select(UserGroup).filter_by(id=self.tim_member.group_id)) .scalars() .one() ) diff --git a/timApp/messaging/timMessage/routes.py b/timApp/messaging/timMessage/routes.py index 047469ab20..dd6ebaa093 100644 --- a/timApp/messaging/timMessage/routes.py +++ b/timApp/messaging/timMessage/routes.py @@ -216,7 +216,8 @@ def get_tim_messages_as_list(item_id: int | None = None) -> list[TimMessageData] .filter((is_global | is_user_specific) & can_see) ) - messages: Sequence[InternalMessage] = run_sql(stmt).scalars().all() + # Make unique because each message contains multiple readreceipts + messages: Sequence[InternalMessage] = run_sql(stmt).unique().scalars().all() full_messages = [] for message in messages: @@ -250,11 +251,10 @@ def get_tim_messages_as_list(item_id: int | None = None) -> list[TimMessageData] for read_receipt in message.readreceipts: read_receipt.last_seen = now else: - db.session.add( - InternalMessageReadReceipt( - message=message, user=cur_user, last_seen=now - ) + receipt = InternalMessageReadReceipt( + message=message, user=cur_user, last_seen=now ) + db.session.add(receipt) db.session.commit() @@ -570,7 +570,7 @@ def get_read_receipts( ) .join(InternalMessage) .filter(InternalMessage.doc_id == doc.id) - ) + ).all() read_user_map: dict[int, datetime] = { user_id: read_time for user_id, read_time, _ in read_users if read_time diff --git a/timApp/user/user.py b/timApp/user/user.py index c00157f383..ba90a88ead 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -305,7 +305,7 @@ class User(DbModel, TimeStampMixin, SCIMEntity): """Personal unique codes used to identify the user via Haka Identity Provider.""" internalmessage_readreceipt: Mapped[ - Optional["InternalMessageReadReceipt"] + List["InternalMessageReadReceipt"] ] = relationship(back_populates="user") """User's read receipts for internal messages.""" From fe5b7e9a43f6324d47fb89d72ca3ccafa42bdf4b Mon Sep 17 00:00:00 2001 From: dezhidki Date: Thu, 14 Sep 2023 10:10:17 +0300 Subject: [PATCH 31/34] tim: Fix Chromedriver download, update package lock --- poetry.lock | 113 +++++++++++++++++++++++++++++++++++++++++++++- timApp/Dockerfile | 15 +++--- 2 files changed, 120 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2cf8575569..f45820201a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2061,6 +2061,40 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "numpy" +version = "1.25.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, + {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, + {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, + {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, + {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, + {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, + {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, + {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, + {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, + {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, + {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, + {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, +] + [[package]] name = "oauth2client" version = "4.1.3" @@ -2609,6 +2643,45 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "qulacs" +version = "0.6.2" +description = "Quantum circuit simulator for research" +optional = false +python-versions = "*" +files = [ + {file = "qulacs-0.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84add575af0fea63dcb15147043aac270079a3cfeacfeffd60244397b34b1dd1"}, + {file = "qulacs-0.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:942726ce5caeacb49204d83fa2b8e2e4ef8110c6dce66e5fcde095a615b4eb37"}, + {file = "qulacs-0.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2523d44970d1a8b398c8b259c63249ee5ddffeeb4b81ab5c9ff46b3444e09678"}, + {file = "qulacs-0.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:eb9a7b44fa1c749132ff9a9221426c4804038e3d15dcdcfe404920ea45641bd4"}, + {file = "qulacs-0.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e143fa8eb2f713fdd662173d2d6e8b6092a438a7fd2ab88368f802ca9d5671f7"}, + {file = "qulacs-0.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2e12dda71a786b56c74548ba910121ac588283b7d9027bf5b0e1abe4ba86ae9"}, + {file = "qulacs-0.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:846aa2f27125ed75e982ec00e27398eda6076084fabc0a8af3d6d7256d3aa2e9"}, + {file = "qulacs-0.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4d2659ce0f8a6c90661ae38856202a8f1dc425b50c7857958ae4ac2e0c65471"}, + {file = "qulacs-0.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d84cc1a79b667445bf7c7c44c6e9213d1393851adaa587b356f97141f3e45ac"}, + {file = "qulacs-0.6.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e661fde991de47e9db229db86a68671548c63e8bdd84b453571594467b288ef7"}, + {file = "qulacs-0.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:94414b5b76dbd48224b499383564a76ed9520c19f94f5892a20830826a712592"}, + {file = "qulacs-0.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:835f527ecdcdc2655f0560513fec3a8c9f28986adefe8d3f09852f82d1cf6103"}, + {file = "qulacs-0.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8bcc97aef9f93dfef9d2c464870526880cc381e58b5f698783b76528589d4982"}, + {file = "qulacs-0.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db3a0db3cd7b3c0b83a2c5e471fcc6147eef2b3053c60b6755396fcc6ed6b356"}, + {file = "qulacs-0.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:390d522701d124af3b6ff1a03f9514e031386272adfa7851416dcfa67226862c"}, + {file = "qulacs-0.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5633e451b154a94eb3a58351f5f37b1312da7b2143c4847e549e001e522bfeb"}, + {file = "qulacs-0.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:802d38b6338f6aeb18204bb302ee30d4c4ab603e171a83dc66aef6fcc832d911"}, + {file = "qulacs-0.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d020c6c7a3046d5b0a8c2e09913308f0b7df4f1f2773e7ef016b62878295dff"}, + {file = "qulacs-0.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:6786439f13768a3849e5abb6e1d960362a3cc0157910f9e83aae28df4e9656d4"}, + {file = "qulacs-0.6.2.tar.gz", hash = "sha256:a459b1b55652fa82d716fac360ea35a8edf6fd2864112105d5ea9b9f169c51bb"}, +] + +[package.dependencies] +numpy = "*" +scipy = "*" + +[package.extras] +ci = ["black", "flake8", "isort", "mypy", "openfermion", "pybind11-stubgen", "pytest"] +dev = ["black", "flake8", "isort", "mypy", "openfermion", "pybind11-stubgen", "pytest"] +doc = ["breathe (==4.33.*)", "exhale (==0.3.*)", "ipykernel (==6.17.*)", "myst-parser (==0.18.*)", "nbsphinx (==0.8.*)", "sphinx (==4.5.0)", "sphinx-autoapi (==2.0.*)", "sphinx-copybutton (==0.5.*)", "sphinx-rtd-theme (==1.0.*)"] +test = ["openfermion"] + [[package]] name = "recommonmark" version = "0.7.1" @@ -2698,6 +2771,44 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" +[[package]] +name = "scipy" +version = "1.9.3" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, + {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, + {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, + {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, + {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, + {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, + {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, + {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, + {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, + {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, + {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, + {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, + {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, + {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, + {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, + {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, + {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, + {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, + {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, + {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, + {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, +] + +[package.dependencies] +numpy = ">=1.18.5,<1.26.0" + +[package.extras] +dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] +doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] +test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + [[package]] name = "selenium" version = "4.10.0" @@ -3677,4 +3788,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "a6065162074bdfebee2a8cb4feb62fdfd60d80c7c529122b770c8bf935158aa6" +content-hash = "af904fa16bb82d9728a7895c70f239f9cbf4c6062a4076e58040ed3599a6724b" diff --git a/timApp/Dockerfile b/timApp/Dockerfile index 999a781193..0b983996a8 100755 --- a/timApp/Dockerfile +++ b/timApp/Dockerfile @@ -176,6 +176,10 @@ RUN sed -i "s/from werkzeug import cached_property/from werkzeug.utils import ca RUN wget -q https://www.texlive.info/CTAN/support/latexmk/latexmk.pl -O /usr/bin/latexmk +RUN bash -c "${APT_INSTALL} jq && ${APT_CLEANUP}" +RUN bash -c "${APT_INSTALL} ripgrep && ${APT_CLEANUP}" +RUN bash -c "${PIP_INSTALL} coverage && ${APT_CLEANUP}" + # Chromedriver (for running tests) # Taken from https://github.com/SeleniumHQ/docker-selenium/blob/trunk/NodeChrome/Dockerfile ARG CHROME_VERSION="google-chrome-stable" @@ -191,14 +195,14 @@ RUN wget -q https://raw.githubusercontent.com/SeleniumHQ/docker-selenium/trunk/N RUN /usr/local/bin/wrap_chrome_binary ARG CHROME_DRIVER_VERSION -RUN bash -c "${APT_INSTALL} jq && ${APT_CLEANUP}" RUN if [ ! -z "$CHROME_DRIVER_VERSION" ]; \ then CHROME_DRIVER_URL=https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/$CHROME_DRIVER_VERSION/linux64/chromedriver-linux64.zip ; \ - else echo "Getting ChromeDriver binary from https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" \ + else echo "Geting ChromeDriver binary from https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" \ && CFT_URL=https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json \ && CFT_CHANNEL="Stable" \ - && if [ "$(echo "$CHROME_VERSION" | grep -q "beta")" ]; then CFT_CHANNEL="Beta"; fi \ - && if [ "$(echo "$CHROME_VERSION" | grep -q "unstable")" ]; then CFT_CHANNEL="Dev"; fi \ + && if [ "$CHROME_VERSION" = "google-chrome-beta" ]; then CFT_CHANNEL="Beta" ; fi \ + && if [ "$CHROME_VERSION" = "google-chrome-unstable" ]; then CFT_CHANNEL="Dev" ; fi \ + && echo $CFT_CHANNEL \ && CTF_VALUES=$(curl -sSL $CFT_URL | jq -r --arg CFT_CHANNEL "$CFT_CHANNEL" '.channels[] | select (.channel==$CFT_CHANNEL)') \ && CHROME_DRIVER_VERSION=$(echo $CTF_VALUES | jq -r '.version' ) \ && CHROME_DRIVER_URL=$(echo $CTF_VALUES | jq -r '.downloads.chromedriver[] | select(.platform=="linux64") | .url' ) ; \ @@ -213,9 +217,6 @@ RUN if [ ! -z "$CHROME_DRIVER_VERSION" ]; \ && chmod 755 /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION \ && ln -fs /opt/selenium/chromedriver-$CHROME_DRIVER_VERSION /usr/bin/chromedriver -RUN bash -c "${APT_INSTALL} ripgrep && ${APT_CLEANUP}" -RUN bash -c "${PIP_INSTALL} coverage && ${APT_CLEANUP}" - WORKDIR /service CMD python3 launch.py From fa577f1c6bf454e65f0c9d53bb18625e5f712337 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Fri, 15 Sep 2023 13:00:23 +0300 Subject: [PATCH 32/34] Fix regression in TIM messages and velps after SQA update --- timApp/static/scripts/tim/messaging/tim-message.component.ts | 2 +- timApp/velp/velps.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/timApp/static/scripts/tim/messaging/tim-message.component.ts b/timApp/static/scripts/tim/messaging/tim-message.component.ts index c25db9ba50..5ac79c0982 100644 --- a/timApp/static/scripts/tim/messaging/tim-message.component.ts +++ b/timApp/static/scripts/tim/messaging/tim-message.component.ts @@ -159,7 +159,7 @@ export class TimMessageComponent implements OnInit { const result = await toPromise( this.http.post("/timMessage/reply", { options: this.replyOptions, - messageBody: { + message: { messageBody: this.replyMessage, messageSubject: `[Re] ${this.message.message_subject}`, recipients: [this.replyOptions.recipient], diff --git a/timApp/velp/velps.py b/timApp/velp/velps.py index 13c3c34aa9..1c536671de 100644 --- a/timApp/velp/velps.py +++ b/timApp/velp/velps.py @@ -254,7 +254,7 @@ def get_velp_content_for_document( .options(selectinload(Velp.groups).raiseload(VelpGroup.block)) .options(selectinload(Velp.velp_versions).joinedload(VelpVersion.content)) ) - return list(run_sql(vq).scalars().all()) + return list(run_sql(vq).unique().scalars().all()) def get_velp_label_content_for_document( @@ -289,6 +289,7 @@ def get_velp_label_content_for_document( .filter_by(doc_id=doc_id, user_id=user_id) .with_only_columns(VelpLabelContent) ) + .unique() .scalars() .all() ) From 1acc33bea7b81645edb130ea7019b69b7b1b2fe2 Mon Sep 17 00:00:00 2001 From: dezhidki Date: Fri, 15 Sep 2023 19:08:35 +0300 Subject: [PATCH 33/34] Update Python packages, fix formatting and mypy errors, move back to db.Model --- poetry.lock | 1283 ++++++++--------- pyproject.toml | 3 +- timApp/Dockerfile | 2 +- timApp/answer/answer.py | 8 +- timApp/answer/answer_models.py | 8 +- timApp/answer/answers.py | 2 +- timApp/auth/access/routes.py | 6 +- timApp/auth/auth_models.py | 8 +- timApp/auth/oauth2/models.py | 6 +- timApp/auth/session/model.py | 4 +- timApp/document/docentry.py | 3 +- timApp/document/documents.py | 1 - timApp/document/editing/routes.py | 1 + timApp/document/translation/language.py | 3 +- .../translation/reversingtranslator.py | 3 +- timApp/document/translation/translation.py | 3 +- timApp/document/translation/translator.py | 7 +- timApp/folder/folder.py | 3 +- timApp/item/block.py | 4 +- timApp/item/blockassociation.py | 4 +- timApp/item/blockrelevance.py | 4 +- timApp/item/tag.py | 5 +- timApp/item/taskblock.py | 3 +- timApp/lecture/askedjson.py | 5 +- timApp/lecture/askedquestion.py | 4 +- timApp/lecture/lecture.py | 4 +- timApp/lecture/lectureanswer.py | 4 +- timApp/lecture/lectureusers.py | 4 +- timApp/lecture/message.py | 5 +- timApp/lecture/question.py | 4 +- timApp/lecture/questionactivity.py | 4 +- timApp/lecture/runningquestion.py | 5 +- timApp/lecture/showpoints.py | 4 +- timApp/lecture/useractivity.py | 4 +- .../messagelist/messagelist_models.py | 20 +- timApp/messaging/messagelist/routes.py | 4 +- .../timMessage/internalmessage_models.py | 10 +- timApp/messaging/timMessage/routes.py | 6 +- timApp/note/usernote.py | 4 +- timApp/notification/notification.py | 5 +- timApp/notification/pending_notification.py | 6 +- timApp/peerreview/peerreview.py | 4 +- timApp/peerreview/util/groups.py | 2 +- timApp/plugin/calendar/models.py | 16 +- timApp/plugin/jsrunner/util.py | 10 +- timApp/plugin/plugintype.py | 3 +- timApp/plugin/quantum_circuit/__init__.py | 0 .../plugin/quantum_circuit/quantumCircuit.py | 24 +- timApp/plugin/tableform/tableForm.py | 2 +- timApp/plugin/timtable/row_owner_info.py | 4 +- timApp/printing/printeddoc.py | 4 +- timApp/readmark/readparagraph.py | 5 +- timApp/sisu/scim.py | 8 +- timApp/sisu/scimusergroup.py | 4 +- timApp/slide/slidestatus.py | 4 +- timApp/timdb/sqa.py | 8 +- timApp/timdb/types.py | 13 +- timApp/user/consentchange.py | 5 +- timApp/user/hakaorganization.py | 3 +- timApp/user/newuser.py | 5 +- timApp/user/personaluniquecode.py | 5 +- timApp/user/settings/settings.py | 2 +- timApp/user/user.py | 15 +- timApp/user/usercontact.py | 3 +- timApp/user/usergroup.py | 3 +- timApp/user/usergroupdoc.py | 4 +- timApp/user/usergroupmember.py | 5 +- timApp/user/verification/verification.py | 8 +- timApp/util/error_handlers.py | 9 +- timApp/velp/annotation.py | 10 +- timApp/velp/annotation_model.py | 5 +- timApp/velp/velp.py | 1 + timApp/velp/velp_models.py | 29 +- timApp/velp/velpgroups.py | 2 +- 74 files changed, 848 insertions(+), 853 deletions(-) create mode 100644 timApp/plugin/quantum_circuit/__init__.py diff --git a/poetry.lock b/poetry.lock index f45820201a..6e11540f6c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -135,13 +135,13 @@ files = [ [[package]] name = "alembic" -version = "1.11.1" +version = "1.12.0" description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.7" files = [ - {file = "alembic-1.11.1-py3-none-any.whl", hash = "sha256:dc871798a601fab38332e38d6ddb38d5e734f60034baeb8e2db5b642fccd8ab8"}, - {file = "alembic-1.11.1.tar.gz", hash = "sha256:6a810a6b012c88b33458fceb869aef09ac75d6ace5291915ba7fae44de372c01"}, + {file = "alembic-1.12.0-py3-none-any.whl", hash = "sha256:03226222f1cf943deee6c85d9464261a6c710cd19b4fe867a3ad1f25afda610f"}, + {file = "alembic-1.12.0.tar.gz", hash = "sha256:8e7645c32e4f200675e69f0745415335eb59a3663f5feb487abfa0b30c45888b"}, ] [package.dependencies] @@ -168,13 +168,13 @@ vine = ">=5.0.0" [[package]] name = "async-timeout" -version = "4.0.2" +version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [[package]] @@ -211,13 +211,13 @@ cryptography = ">=3.2" [[package]] name = "autopep8" -version = "2.0.2" +version = "2.0.4" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" optional = false python-versions = ">=3.6" files = [ - {file = "autopep8-2.0.2-py2.py3-none-any.whl", hash = "sha256:86e9303b5e5c8160872b2f5ef611161b2893e9bfe8ccc7e2f76385947d57a2f1"}, - {file = "autopep8-2.0.2.tar.gz", hash = "sha256:f9849cdd62108cb739dbcdbfb7fdcc9a30d1b63c4cc3e1c1f893b5360941b61c"}, + {file = "autopep8-2.0.4-py2.py3-none-any.whl", hash = "sha256:067959ca4a07b24dbd5345efa8325f5f58da4298dab0dde0443d5ed765de80cb"}, + {file = "autopep8-2.0.4.tar.gz", hash = "sha256:2913064abd97b3419d1cc83ea71f042cb821f87e45b9c88cad5ad3c4ea87fe0c"}, ] [package.dependencies] @@ -299,33 +299,33 @@ files = [ [[package]] name = "black" -version = "23.7.0" +version = "23.9.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, - {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, - {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, - {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, - {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, - {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, - {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, - {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, - {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, - {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, - {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, - {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, - {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, - {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, - {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, - {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"}, + {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"}, + {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"}, + {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"}, + {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"}, + {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"}, + {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"}, + {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"}, + {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"}, + {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"}, + {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"}, ] [package.dependencies] @@ -373,94 +373,134 @@ files = [ [[package]] name = "brotli" -version = "1.0.9" +version = "1.1.0" description = "Python bindings for the Brotli compression library" optional = false python-versions = "*" files = [ - {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"}, - {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"}, - {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"}, - {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"}, - {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"}, - {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"}, - {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"}, - {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"}, - {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"}, - {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"}, - {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"}, - {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"}, - {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"}, - {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"}, - {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"}, - {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"}, - {file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cc0283a406774f465fb45ec7efb66857c09ffefbe49ec20b7882eff6d3c86d3a"}, - {file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:11d3283d89af7033236fa4e73ec2cbe743d4f6a81d41bd234f24bf63dde979df"}, - {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1306004d49b84bd0c4f90457c6f57ad109f5cc6067a9664e12b7b79a9948ad"}, - {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1375b5d17d6145c798661b67e4ae9d5496920d9265e2f00f1c2c0b5ae91fbde"}, - {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cab1b5964b39607a66adbba01f1c12df2e55ac36c81ec6ed44f2fca44178bf1a"}, - {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ed6a5b3d23ecc00ea02e1ed8e0ff9a08f4fc87a1f58a2530e71c0f48adf882f"}, - {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cb02ed34557afde2d2da68194d12f5719ee96cfb2eacc886352cb73e3808fc5d"}, - {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b3523f51818e8f16599613edddb1ff924eeb4b53ab7e7197f85cbc321cdca32f"}, - {file = "Brotli-1.0.9-cp311-cp311-win32.whl", hash = "sha256:ba72d37e2a924717990f4d7482e8ac88e2ef43fb95491eb6e0d124d77d2a150d"}, - {file = "Brotli-1.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:3ffaadcaeafe9d30a7e4e1e97ad727e4f5610b9fa2f7551998471e3736738679"}, - {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"}, - {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"}, - {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"}, - {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"}, - {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"}, - {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"}, - {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"}, - {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"}, - {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"}, - {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"}, - {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"}, - {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"}, - {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"}, - {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"}, - {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"}, - {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"}, - {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"}, - {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"}, - {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"}, - {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"}, - {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"}, - {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"}, - {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"}, - {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"}, - {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"}, - {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"}, - {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"}, - {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"}, - {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"}, - {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"}, - {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"}, - {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"}, - {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"}, - {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"}, - {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"}, - {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"}, - {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"}, - {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"}, - {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"}, - {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"}, - {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"}, - {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"}, - {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"}, - {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"}, - {file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73fd30d4ce0ea48010564ccee1a26bfe39323fde05cb34b5863455629db61dc7"}, - {file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02177603aaca36e1fd21b091cb742bb3b305a569e2402f1ca38af471777fb019"}, - {file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"}, - {file = "Brotli-1.0.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b43775532a5904bc938f9c15b77c613cb6ad6fb30990f3b0afaea82797a402d8"}, - {file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5bf37a08493232fbb0f8229f1824b366c2fc1d02d64e7e918af40acd15f3e337"}, - {file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:330e3f10cd01da535c70d09c4283ba2df5fb78e915bea0a28becad6e2ac010be"}, - {file = "Brotli-1.0.9-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e1abbeef02962596548382e393f56e4c94acd286bd0c5afba756cffc33670e8a"}, - {file = "Brotli-1.0.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3148362937217b7072cf80a2dcc007f09bb5ecb96dae4617316638194113d5be"}, - {file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336b40348269f9b91268378de5ff44dc6fbaa2268194f85177b53463d313842a"}, - {file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b09a16a1950b9ef495a0f8b9d0a87599a9d1f179e2d4ac014b2ec831f87e7"}, - {file = "Brotli-1.0.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c8e521a0ce7cf690ca84b8cc2272ddaf9d8a50294fd086da67e517439614c755"}, - {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"}, -] + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "brotlicffi" +version = "1.1.0.0" +description = "Python CFFI bindings to the Brotli library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, + {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, +] + +[package.dependencies] +cffi = ">=1.0.0" [[package]] name = "cachelib" @@ -475,13 +515,13 @@ files = [ [[package]] name = "celery" -version = "5.3.1" +version = "5.3.4" description = "Distributed Task Queue." optional = false python-versions = ">=3.8" files = [ - {file = "celery-5.3.1-py3-none-any.whl", hash = "sha256:27f8f3f3b58de6e0ab4f174791383bbd7445aff0471a43e99cfd77727940753f"}, - {file = "celery-5.3.1.tar.gz", hash = "sha256:f84d1c21a1520c116c2b7d26593926581191435a03aa74b77c941b93ca1c6210"}, + {file = "celery-5.3.4-py3-none-any.whl", hash = "sha256:1e6ed40af72695464ce98ca2c201ad0ef8fd192246f6c9eac8bba343b980ad34"}, + {file = "celery-5.3.4.tar.gz", hash = "sha256:9023df6a8962da79eb30c0c84d5f4863d9793a466354cc931d7f72423996de28"}, ] [package.dependencies] @@ -490,15 +530,15 @@ click = ">=8.1.2,<9.0" click-didyoumean = ">=0.3.0" click-plugins = ">=1.1.1" click-repl = ">=0.2.0" -kombu = ">=5.3.1,<6.0" +kombu = ">=5.3.2,<6.0" python-dateutil = ">=2.8.2" -redis = {version = ">=4.5.2,<4.5.5 || >4.5.5", optional = true, markers = "extra == \"redis\""} +redis = {version = ">=4.5.2,<4.5.5 || >4.5.5,<5.0.0", optional = true, markers = "extra == \"redis\""} tzdata = ">=2022.7" vine = ">=5.0.0,<6.0" [package.extras] -arangodb = ["pyArango (>=2.0.1)"] -auth = ["cryptography (==41.0.1)"] +arangodb = ["pyArango (>=2.0.2)"] +auth = ["cryptography (==41.0.3)"] azureblockblob = ["azure-storage-blob (>=12.15.0)"] brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] cassandra = ["cassandra-driver (>=3.25.0,<4)"] @@ -518,7 +558,7 @@ msgpack = ["msgpack (==1.0.5)"] pymemcache = ["python-memcached (==1.59)"] pyro = ["pyro4 (==4.82)"] pytest = ["pytest-celery (==0.0.0)"] -redis = ["redis (>=4.5.2,!=4.5.5)"] +redis = ["redis (>=4.5.2,!=4.5.5,<5.0.0)"] s3 = ["boto3 (>=1.26.143)"] slmq = ["softlayer-messaging (>=1.0.3)"] solar = ["ephem (==4.1.4)"] @@ -702,13 +742,13 @@ files = [ [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -790,34 +830,34 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "cryptography" -version = "41.0.2" +version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, - {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, - {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, - {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, ] [package.dependencies] @@ -900,13 +940,13 @@ dev = ["Sphinx", "coverage", "flake8", "lxml", "lxml-stubs", "memory-profiler", [[package]] name = "exceptiongroup" -version = "1.1.2" +version = "1.1.3" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, ] [package.extras] @@ -914,28 +954,29 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.12.2" +version = "3.12.4" description = "A platform independent file lock." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, + {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, + {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] +typing = ["typing-extensions (>=4.7.1)"] [[package]] name = "flask" -version = "2.3.2" +version = "2.3.3" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.8" files = [ - {file = "Flask-2.3.2-py3-none-any.whl", hash = "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0"}, - {file = "Flask-2.3.2.tar.gz", hash = "sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef"}, + {file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"}, + {file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"}, ] [package.dependencies] @@ -943,7 +984,7 @@ blinker = ">=1.6.2" click = ">=8.1.3" itsdangerous = ">=2.1.2" Jinja2 = ">=3.1.2" -Werkzeug = ">=2.3.3" +Werkzeug = ">=2.3.7" [package.extras] async = ["asgiref (>=3.2)"] @@ -981,28 +1022,29 @@ Flask = "<3" [[package]] name = "flask-compress" -version = "1.13" +version = "1.14" description = "Compress responses in your Flask app with gzip, deflate or brotli." optional = false python-versions = "*" files = [ - {file = "Flask-Compress-1.13.tar.gz", hash = "sha256:ee96f18bf9b00f2deb4e3406ca4a05093aa80e2ef0578525a3b4d32ecdff129d"}, - {file = "Flask_Compress-1.13-py3-none-any.whl", hash = "sha256:1128f71fbd788393ce26830c51f8b5a1a7a4d085e79a21a5cddf4c057dcd559b"}, + {file = "Flask-Compress-1.14.tar.gz", hash = "sha256:e46528f37b91857012be38e24e65db1a248662c3dc32ee7808b5986bf1d123ee"}, + {file = "Flask_Compress-1.14-py3-none-any.whl", hash = "sha256:b86c9808f0f38ea2246c9730972cf978f2cdf6a9a1a69102ba81e07891e6b26c"}, ] [package.dependencies] -brotli = "*" +brotli = {version = "*", markers = "platform_python_implementation != \"PyPy\""} +brotlicffi = {version = "*", markers = "platform_python_implementation == \"PyPy\""} flask = "*" [[package]] name = "flask-migrate" -version = "4.0.4" +version = "4.0.5" description = "SQLAlchemy database migrations for Flask applications using Alembic." optional = false python-versions = ">=3.6" files = [ - {file = "Flask-Migrate-4.0.4.tar.gz", hash = "sha256:73293d40b10ac17736e715b377e7b7bde474cb8105165d77474df4c3619b10b3"}, - {file = "Flask_Migrate-4.0.4-py3-none-any.whl", hash = "sha256:77580f27ab39bc68be4906a43c56d7674b45075bc4f883b1d0b985db5164d58f"}, + {file = "Flask-Migrate-4.0.5.tar.gz", hash = "sha256:d3f437a8b5f3849d1bb1b60e1b818efc564c66e3fefe90b62e5db08db295e1b1"}, + {file = "Flask_Migrate-4.0.5-py3-none-any.whl", hash = "sha256:613a2df703998e78716cace68cd83972960834424457f5b67f56e74fff950aef"}, ] [package.dependencies] @@ -1012,19 +1054,19 @@ Flask-SQLAlchemy = ">=1.0" [[package]] name = "flask-oidc" -version = "1.4.0" +version = "2.0.3" description = "OpenID Connect extension for Flask" optional = false -python-versions = "*" +python-versions = ">=3.8,<4.0" files = [ - {file = "flask-oidc-1.4.0.tar.gz", hash = "sha256:0c12151139d47a562e1c5ae203fb9dbc759fe7474cc01e0238bef828ece58f4e"}, + {file = "flask_oidc-2.0.3-py3-none-any.whl", hash = "sha256:b83e08a3768290de933db24e1e459c1754634430cd5a132af2c4969da9d1764e"}, + {file = "flask_oidc-2.0.3.tar.gz", hash = "sha256:07b36ba093707d88012c7b1d1f21f6ef91063cf75b79ee5330c02fdebca44d19"}, ] [package.dependencies] -Flask = "*" -itsdangerous = "*" -oauth2client = "*" -six = "*" +authlib = ">=1.2.0,<2.0.0" +flask = ">=2.0.0,<3.0.0" +requests = ">=2.24.0,<3.0.0" [[package]] name = "flask-openid" @@ -1043,18 +1085,18 @@ python3-openid = ">=2.0" [[package]] name = "flask-sqlalchemy" -version = "3.0.5" +version = "3.1.1" description = "Add SQLAlchemy support to your Flask application." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "flask_sqlalchemy-3.0.5-py3-none-any.whl", hash = "sha256:cabb6600ddd819a9f859f36515bb1bd8e7dbf30206cc679d2b081dff9e383283"}, - {file = "flask_sqlalchemy-3.0.5.tar.gz", hash = "sha256:c5765e58ca145401b52106c0f46178569243c5da25556be2c231ecc60867c5b1"}, + {file = "flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0"}, + {file = "flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312"}, ] [package.dependencies] flask = ">=2.2.5" -sqlalchemy = ">=1.4.18" +sqlalchemy = ">=2.0.16" [[package]] name = "flask-testing" @@ -1160,50 +1202,56 @@ files = [ [[package]] name = "gevent" -version = "23.7.0" +version = "23.9.1" description = "Coroutine-based network library" optional = false python-versions = ">=3.8" files = [ - {file = "gevent-23.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:add904a7ef960cd4e133e61eb7413982c5e4203928160be1c09752ac06a25e71"}, - {file = "gevent-23.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bd9ea1b5fbdc7e5921a9e515f34a450eb3927a902253a33caedcce2d19d7d96"}, - {file = "gevent-23.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c7c349aa23d67cf5cc3b2c87aaedcfead976d0577b1cfcd07ffeba63baba79c"}, - {file = "gevent-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92b837b60e850c50fc6d723d1e363e786d37fd9d51e564e07df52ad5e8a86d4"}, - {file = "gevent-23.7.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6a51a8e3cdaa6901e47d56f84cb5f92b1bf3deea920bce69cf7a245df16159ac"}, - {file = "gevent-23.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1dba07207b15b371e50372369edf256a142cb5cdf8599849cbf8660327efa06"}, - {file = "gevent-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:34086bcc1252ae41e1cb81cf13c4a7678031595c12f4e9a1c3d0ab433f20826a"}, - {file = "gevent-23.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5da07d65dfa23fe419c37cea110bf951b42af6bf3a1fff244043a75c9185dbd5"}, - {file = "gevent-23.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4d7be3352126458cc818309ca6a3b678c209b1ae33e56b6975c6a8309f2068"}, - {file = "gevent-23.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76ca6f893953ab898ebbff5d772103318a85044e55d0bad401d6b49d71bb76e7"}, - {file = "gevent-23.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aeb1511cf0786152af741c47ee462dac81b57bbd1fbbe08ab562b6c8c9ad75ed"}, - {file = "gevent-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:919423e803939726c99ab2d29ea46b8676af549cee72d263f2b24758ec607b2c"}, - {file = "gevent-23.7.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cea93f4f77badbddc711620cca164ad75c74056603908e621a5ba1b97adbc39c"}, - {file = "gevent-23.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dec7b08daf08385fb281b81ec2e7e703243975d867f40ae0a8a3e30b380eb9ea"}, - {file = "gevent-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:f522b6b015f1bfa9d8d3716ddffb23e3d4a8933df3e4ebf0a29a65a9fa74382b"}, - {file = "gevent-23.7.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:746a1e12f280dab07389e6709164b1e1a6caaf50493ea5b1dcaa73cff005174c"}, - {file = "gevent-23.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b230007a665d2cf5cf8878c9f56a2b8bacbdc4fe0235afc5269b71cd00528e5"}, - {file = "gevent-23.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1d2f1e67d04fde47ca2deac89733df28ef3a7ec1d7359a79f57d4778cced16d"}, - {file = "gevent-23.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:debc177e88a8c876cb1a4d974f985d03670177bdc61e1c084a8d525f1a50b12d"}, - {file = "gevent-23.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b3dd449c80814357f6568eb095a2be2421b805d59fa97c65094707e04a181f9"}, - {file = "gevent-23.7.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:769e8811ded08fe7d8b09ad8ebb72d47aecc112411e0726e7296b7ed187ed629"}, - {file = "gevent-23.7.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11b9bb0bce45170ff992760385a86e6955ccb88dba4a82a64d5ce9459290d8d6"}, - {file = "gevent-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e0d76a7848726e0646324a1adc011355dcd91875e7913badd1ada2e5eeb8a6e"}, - {file = "gevent-23.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a226b42cb9a49580ca7729572a4f8289d1fa28cd2529c9f4eed3e14b995d1c9c"}, - {file = "gevent-23.7.0-cp38-cp38-win32.whl", hash = "sha256:1234849b0bc4df560924aa92f7c01ca3f310677735fb508a2b0d7a61bb946916"}, - {file = "gevent-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:a8f62e8d37913512823923e05607a296389aeb50ccca8a271ae7cedb5b17faeb"}, - {file = "gevent-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369241d1a6a3fe3ef4eba454b71e0168026560c5344fc4bc37196867041982ac"}, - {file = "gevent-23.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:94b013587f7c4697d620c129627f7b12d7d9f6e40ab198635891ca2098cd8556"}, - {file = "gevent-23.7.0-cp39-cp39-win32.whl", hash = "sha256:83b6d61a8e9da25edb304ca7fba19ee57bb1ffa801f9df3e668bfed7bb8386cb"}, - {file = "gevent-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:8c284390f0f6d0b5be3bf805fa8e0ae1329065f2b0ac5af5423c67183197deb8"}, - {file = "gevent-23.7.0.tar.gz", hash = "sha256:d0d3630674c1b344b256a298ab1ff43220f840b12af768131b5d74e485924237"}, + {file = "gevent-23.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae"}, + {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6"}, + {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7"}, + {file = "gevent-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e"}, + {file = "gevent-23.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7"}, + {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71"}, + {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e"}, + {file = "gevent-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a"}, + {file = "gevent-23.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599"}, + {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"}, + {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d"}, + {file = "gevent-23.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1"}, + {file = "gevent-23.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe"}, + {file = "gevent-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5"}, + {file = "gevent-23.9.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397"}, + {file = "gevent-23.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507"}, + {file = "gevent-23.9.1-cp38-cp38-win32.whl", hash = "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a"}, + {file = "gevent-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f"}, + {file = "gevent-23.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a"}, + {file = "gevent-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653"}, + {file = "gevent-23.9.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd"}, + {file = "gevent-23.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543"}, + {file = "gevent-23.9.1-cp39-cp39-win32.whl", hash = "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2"}, + {file = "gevent-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b"}, + {file = "gevent-23.9.1.tar.gz", hash = "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34"}, ] [package.dependencies] cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} -greenlet = [ - {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.12\""}, - {version = ">=3.0a1", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.12\""}, -] +greenlet = {version = ">=3.0rc3", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\""} "zope.event" = "*" "zope.interface" = "*" @@ -1216,70 +1264,69 @@ test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idn [[package]] name = "greenlet" -version = "3.0.0a1" +version = "3.0.0rc3" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" files = [ - {file = "greenlet-3.0.0a1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8dd92fd76a61af2abc8ccad0c6c6069b3c4ebd4727ecc9a7c33aae37651c8c7"}, - {file = "greenlet-3.0.0a1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:889934aa8d72b6bfc46babd1dc4b817a56c97ec0f4a10ae7551fb60ab1f96fae"}, - {file = "greenlet-3.0.0a1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b767930af686551dc96a5eb70af3736709d547ffa275c11a5e820bfb3ae61d8d"}, - {file = "greenlet-3.0.0a1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a1f4d256b81f59ba87bb7a29b9b38b1c018e052dba60a543cb0ddb5062d159"}, - {file = "greenlet-3.0.0a1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3fb459ced6c5e3b2a895f23f1400f93e9b24d85c30fbe2d637d4f7706a1116b"}, - {file = "greenlet-3.0.0a1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180ec55cb127bc745669eddc9793ffab6e0cf7311e67e1592f183d6ca00d88c1"}, - {file = "greenlet-3.0.0a1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab81f9ff3e3c2ca65e824454214c10985a846cd9bee5f4d04e15cd875d9fe13b"}, - {file = "greenlet-3.0.0a1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:21ebcb570e0d8501457d6a2695a44c5af3b6c2143dc6644ec73574beba067c90"}, - {file = "greenlet-3.0.0a1-cp310-cp310-win_amd64.whl", hash = "sha256:4d0c0ffd732466ff324ced144fad55ed5deca36f6036c1d8f04cec69b084c9d6"}, - {file = "greenlet-3.0.0a1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4a2d6ed0515c05afd5cc435361ced0baabd9ba4536ddfe8ad9a95bcb702c8ce"}, - {file = "greenlet-3.0.0a1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffb9f8969789771e95d3c982a36be81f0adfaa7302a1d56e29f168ca15e284b8"}, - {file = "greenlet-3.0.0a1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3f3568478bc21b85968e8038c4f98f4bf0039a692791bc324b5e0d1522f4b1"}, - {file = "greenlet-3.0.0a1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e160a65cc6023a237be870f2072513747d512a1d018efa083acce0b673cccc0"}, - {file = "greenlet-3.0.0a1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e31d1a33dc9006b278f72cb0aacfe397606c2693aa2fdc0c2f2dcddbad9e0b53"}, - {file = "greenlet-3.0.0a1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00550757fca1b9cbc479f8eb1cf3514dbc0103b3f76eae46341c26ddcca67a9"}, - {file = "greenlet-3.0.0a1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2840187a94e258445e62ff1545e34f0b1a14aef4d0078e5c88246688d2b6515e"}, - {file = "greenlet-3.0.0a1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:271ed380389d2f7e4c1545b6e0837986e62504ab561edbaff05da9c9f3f98f96"}, - {file = "greenlet-3.0.0a1-cp311-cp311-win_amd64.whl", hash = "sha256:4ff2a765f4861fc018827eab4df1992f7508d06c62de5d2fe8a6ac2233d4f1d0"}, - {file = "greenlet-3.0.0a1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:463d63ca5d8c236788284a9a44b9715372a64d5318a6b5eee36815df1ea0ba3d"}, - {file = "greenlet-3.0.0a1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3530c0ec1fc98c43d5b7061781a8c55bd0db44f789f8152e19d9526cbed6021"}, - {file = "greenlet-3.0.0a1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bce5cf2b0f0b29680396c5c98ab39a011bd70f2dfa8b8a6811a69ee6d920cf9f"}, - {file = "greenlet-3.0.0a1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5672082576d0e9f52fa0fa732ff57254d65faeb4a471bc339fe54b58b3e79d2"}, - {file = "greenlet-3.0.0a1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5552d7be37d878e9b6359bbffa0512d857bb9703616a4c0656b49c10739d5971"}, - {file = "greenlet-3.0.0a1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:36cebce1f30964d5672fd956860e7e7b69772da69658d5743cb676b442eeff36"}, - {file = "greenlet-3.0.0a1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:665942d3a954c3e4c976581715f57fb3b86f4cf6bae3ac30b133f8ff777ac6c7"}, - {file = "greenlet-3.0.0a1-cp312-cp312-win_amd64.whl", hash = "sha256:ce70aa089ec589b5d5fab388af9f8c9f9dfe8fe4ad844820a92eb240d8628ddf"}, - {file = "greenlet-3.0.0a1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:17503397bf6cbb5e364217143b6150c540020c51a3f6b08f9a20cd67c25e2ca8"}, - {file = "greenlet-3.0.0a1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d61bad421c1f496f9fb6114dbd7c30a1dac0e9ff90e9be06f4472cbd8f7a1704"}, - {file = "greenlet-3.0.0a1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bab71f73001cd15723c4e2ca398f2f48e0a3f584c619eefddb1525e8986e06eb"}, - {file = "greenlet-3.0.0a1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f61df4fe07864561f49b45c8bd4d2c42e3f03d2872ed05c844902a58b875028"}, - {file = "greenlet-3.0.0a1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c02e514c72e745e49a3ae7e672a1018ba9b68460c21e0361054e956e5d595bc6"}, - {file = "greenlet-3.0.0a1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd31ab223e43ac64fd23f8f5dad249addadac2a459f040546200acbf7e84e353"}, - {file = "greenlet-3.0.0a1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6aac94ff957b5dea0216af71ab59c602e1b947b394e4f5e878a5a65643090038"}, - {file = "greenlet-3.0.0a1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d7ba2e5cb119eddbc10874b41047ad99525e39e397f7aef500e6da0d6f46ab91"}, - {file = "greenlet-3.0.0a1-cp37-cp37m-win32.whl", hash = "sha256:ac10196b8cde7a082e4e371ff171407270d3337c8d57ed43030094eb01d9c95c"}, - {file = "greenlet-3.0.0a1-cp37-cp37m-win_amd64.whl", hash = "sha256:0a9dfcadc1d79696e90ccb1275c30ad4ec5fd3d1ab3ae6671286fac78ef33435"}, - {file = "greenlet-3.0.0a1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5942b1d6ba447cff1ec23a21ec525dde2288f00464950bc647f4e0f03bd537d1"}, - {file = "greenlet-3.0.0a1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:450a7e52a515402fd110ba807f1a7d464424bfa703be4effbcb97e1dfbfcc621"}, - {file = "greenlet-3.0.0a1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:df34b52aa50a38d7a79f3abc9fda7e400791447aa0400ed895f275f6d8b0bb1f"}, - {file = "greenlet-3.0.0a1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cda110faee67613fed221f90467003f477088ef1cc84c8fc88537785a5b4de9"}, - {file = "greenlet-3.0.0a1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f313771cb8ee0a04dfdf586b7d4076180d80c94be09049daeea018089b5b957"}, - {file = "greenlet-3.0.0a1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42bfe67824a9b53e73f568f982f0d1d4c7ac0f587d2e702a23f8a7b505d7b7c2"}, - {file = "greenlet-3.0.0a1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0fc20e6e6b298861035a5fc5dcf9fbaa0546318e8bda81112591861a7dcc28f"}, - {file = "greenlet-3.0.0a1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f34ec09702be907727fd479046193725441aaaf7ed4636ca042734f469bb7451"}, - {file = "greenlet-3.0.0a1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:270432cfdd6a50016b8259b3bbf398a3f7c06a06f2c68c7b93e49f53bc193bcf"}, - {file = "greenlet-3.0.0a1-cp38-cp38-win32.whl", hash = "sha256:d47b2e1ad1429da9aa459ef189fbcd8a74ec28a16bc4c3f5f3cf3f88e36535eb"}, - {file = "greenlet-3.0.0a1-cp38-cp38-win_amd64.whl", hash = "sha256:e7b192c3df761d0fdd17c2d42d41c28460f124f5922e8bd524018f1d35610682"}, - {file = "greenlet-3.0.0a1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e20d5e8dc76b73db9280464d6e81bea05e51a99f4d4dd29c5f78dc79f294a5d3"}, - {file = "greenlet-3.0.0a1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:ba94c08321b5d345100fc64eb1ab235f42faf9aabba805cface55ebe677f1c2c"}, - {file = "greenlet-3.0.0a1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:24071eee113d75fedebaeb86264d94f04b5a24e311c5ba3e8003c07d00112a7e"}, - {file = "greenlet-3.0.0a1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:585810056a8adacd3152945ebfcd25deb58335d41f16ae4e0f3d768918957f9a"}, - {file = "greenlet-3.0.0a1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3a99f890f2cc5535e1b3a90049c6ca9ff9da9ec251cc130c8d269997f9d32ee"}, - {file = "greenlet-3.0.0a1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c355c99be5bb23e85d899b059a4f22fdf8a0741c57e7029425ee63eb436f689"}, - {file = "greenlet-3.0.0a1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dde0ab052c7a1deee8d13d72c37f2afecee30ebdf6eb139790157eaddf04dd61"}, - {file = "greenlet-3.0.0a1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ed0f4fad4c3656e34d20323a789b6a2d210a6bb82647d9c86dded372f55c58a1"}, - {file = "greenlet-3.0.0a1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53abf19b7dc62795c67b8d0a3d8ef866db166b21017632fff2624cf8fbf3481c"}, - {file = "greenlet-3.0.0a1-cp39-cp39-win32.whl", hash = "sha256:2fcf7af83516db35af3d0ed5d182dea8585eddd891977adff1b74212f4bfd2fd"}, - {file = "greenlet-3.0.0a1-cp39-cp39-win_amd64.whl", hash = "sha256:68368e908f14887fb202a81960bfbe3a02d97e6d3fa62b821556463084ffb131"}, - {file = "greenlet-3.0.0a1.tar.gz", hash = "sha256:1bd4ea36f0aeb14ca335e0c9594a5aaefa1ac4e2db7d86ba38f0be96166b3102"}, + {file = "greenlet-3.0.0rc3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a2affddff9b2f846f40799673e41b29f0500582415c860fca8f146858e9de1a"}, + {file = "greenlet-3.0.0rc3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd00046dfd00767fce18f9933658d126652a500caf7af9dbfbd43818e4b484c2"}, + {file = "greenlet-3.0.0rc3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e787b00002cef3b98c7cf700fb85c2c01b0d202b1c6731706e5baa4b3325aa1e"}, + {file = "greenlet-3.0.0rc3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ffc7538bc66766a8b551888903d415773481c4bd13560a4fb24887222e3cc9"}, + {file = "greenlet-3.0.0rc3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dde5deb355b34bbf44b15789e27c56862f51f417207be49eedc58fce34681fe6"}, + {file = "greenlet-3.0.0rc3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1fb703a102a02361a0cc6a3d9a7958e1584fdeb536bd37ca9aca529d3356bedd"}, + {file = "greenlet-3.0.0rc3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f8661d14d3e07f2ceeb850e4cbcc7114bdf90a8dc82d63d37b08a50bb6955a77"}, + {file = "greenlet-3.0.0rc3-cp310-cp310-win_amd64.whl", hash = "sha256:997456b74efee91ceeb39d63818909da5dbb712a07f7742f4378986ac3473463"}, + {file = "greenlet-3.0.0rc3-cp310-universal2-macosx_11_0_x86_64.whl", hash = "sha256:d3cd3957af8cec1fcfd87d92ca71b7d434d798036e14ae878f9ab1e07d99da0d"}, + {file = "greenlet-3.0.0rc3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:895b689fc52a5bc402f8d624705110df5c265b1410ffe8e0769a66db9d2e7851"}, + {file = "greenlet-3.0.0rc3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a84a88422b5a0360fae57ad6b3b20fc17c9462880929810b0a26ee43aa05982e"}, + {file = "greenlet-3.0.0rc3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d79cf299ba1996d8a4f133b317e709a0a3ce87181308280e40664e12cb512c54"}, + {file = "greenlet-3.0.0rc3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef9789aea735004eba559c7919a73a3b475d0c28e2c1e9de464c6bc761bf69f4"}, + {file = "greenlet-3.0.0rc3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66790e1537382e53bce64de3a695d1b12a04b00104df45f7ef472a10561936c2"}, + {file = "greenlet-3.0.0rc3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:68349987bf2ce274953f9f9b28458869bd8770a0c5461e1ef91d8107b1bae361"}, + {file = "greenlet-3.0.0rc3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30ffaa6c020a615c8f4be3abfc6029982fda026a3bf9a6dc7205afb033251506"}, + {file = "greenlet-3.0.0rc3-cp311-cp311-win_amd64.whl", hash = "sha256:864619b058f573058cd77f6944cf63d7f42157fe30be494798721bd8ac256d7b"}, + {file = "greenlet-3.0.0rc3-cp311-universal2-macosx_10_9_universal2.whl", hash = "sha256:7c887ecb55374d585d71ff8f9d07c137637694e88fa2b5d5b1450a05ece62ae9"}, + {file = "greenlet-3.0.0rc3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:686821157368c1c4ef53aa68e6801280010da92ab0e4265dad37003341fca6a1"}, + {file = "greenlet-3.0.0rc3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:242d56d2d5f6859f0f086ce62555a2c692c8053c89721d41fead5e1e8dffdb36"}, + {file = "greenlet-3.0.0rc3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81d653ae6c64b85ce4c7bccbea7b630de8799da751b73e55b4c68875b6eb19d6"}, + {file = "greenlet-3.0.0rc3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beeb5cfbd8f3792c37db4e3c5665aa750d78bbdabe758161a34e7dfe27075e69"}, + {file = "greenlet-3.0.0rc3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30daee988fc83078b016fa95a7a1f78a7c86534a44238748b9748675814eb1dc"}, + {file = "greenlet-3.0.0rc3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:977898b8c24159467c66ed1a8f62aacd33f3d85f852cf413d0d2e2a87a6b3091"}, + {file = "greenlet-3.0.0rc3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:097a2f75c79c3fa76fea2e5d48a637233722fe72a5ebb1213c55f0a0898f481c"}, + {file = "greenlet-3.0.0rc3-cp312-cp312-win_amd64.whl", hash = "sha256:5770d43b08dfa10f4460c1bd51f8c80e6f2c47611054e9fb80d4d7976d07e560"}, + {file = "greenlet-3.0.0rc3-cp312-universal2-macosx_10_9_universal2.whl", hash = "sha256:f33e7ff85775cb0ec6abb0950ffc631960bae5a203da38166fc3dfde826e0d0a"}, + {file = "greenlet-3.0.0rc3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f6d1ce31a1db5102a42b4afa609af330edfd8a81d10faba3e47ae33a07cbdf"}, + {file = "greenlet-3.0.0rc3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86e651fa59263f7ff1d4657b086c48cfe7e26db2a36e2d74069f3b5aeab478e6"}, + {file = "greenlet-3.0.0rc3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef7c6e49a9a020d56349c6a769352709bfbe35d3ee7f98bd5efcac6cedbdc162"}, + {file = "greenlet-3.0.0rc3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5585bf8d1d2d3712010ee74988c2ed85c54b127b97f2778fbdcc5b3ea8e801a2"}, + {file = "greenlet-3.0.0rc3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c706041cd92e1b9d2b602eaa31e94aad14453bdbf186ce77530f25167c173a0e"}, + {file = "greenlet-3.0.0rc3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:37213e72058d2e6231d18417adc63c698c040fbb47dc59a3fd633973214ab1ab"}, + {file = "greenlet-3.0.0rc3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:95bc6ec8dd73f8f36e9dfc61a7fa5a2819d1cd52d0bfdb70a43434d6b2aeb239"}, + {file = "greenlet-3.0.0rc3-cp37-cp37m-win32.whl", hash = "sha256:e83c4c7a0814dcfd7e2fe4b74a371f3ce489b62ff02e81d0c5cacc8ba4750395"}, + {file = "greenlet-3.0.0rc3-cp37-cp37m-win_amd64.whl", hash = "sha256:4c35608918f331256be199d3712552fa8a1d12f87ac171a86a31488c60d298f5"}, + {file = "greenlet-3.0.0rc3-cp37-universal2-macosx_11_0_x86_64.whl", hash = "sha256:215bdb33e85fd89fe55f9984dc6f0a96b5774bace663e1a6d051e65d66170ef8"}, + {file = "greenlet-3.0.0rc3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69db00f775ed9d233f53ef67c66ea40a7add0c0929eb528f633982e27595dd37"}, + {file = "greenlet-3.0.0rc3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fcc7162944c2fedfb2253ca2171267e016a3b065c73369d0d4a27f601e7f162"}, + {file = "greenlet-3.0.0rc3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c0082d7b83312c59127811367089f812f8f1386fad7e8cf321fd732b4a6ace6"}, + {file = "greenlet-3.0.0rc3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66f1131c17dba115ea7cb3b257b6751b3c4cfd324f2121447e2483f57abbbf3c"}, + {file = "greenlet-3.0.0rc3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0c5508582339090b99e2863a157fc2708ab9c8b5cd21619bdcb04edcdc6c28d"}, + {file = "greenlet-3.0.0rc3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f1c9ad8d6500f7b142a94054281d9628bc8652a14b0923d02e0dfd87392fbc74"}, + {file = "greenlet-3.0.0rc3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bd586284bbf18ca3068e1fcc67ef54538e1bb74cb605ebdac9e62048237839f5"}, + {file = "greenlet-3.0.0rc3-cp38-cp38-win32.whl", hash = "sha256:1c16f1bbaf9c75dfac3e52bb778d2fd6099fd5aa59fafa678eca5853eedd99ec"}, + {file = "greenlet-3.0.0rc3-cp38-cp38-win_amd64.whl", hash = "sha256:e388ceb55b8f3f388afea4d4a17a64b619040f0e8e9fa3e17e7c34f4d0fbe103"}, + {file = "greenlet-3.0.0rc3-cp38-universal2-macosx_11_0_x86_64.whl", hash = "sha256:68bd35ad9f99df0ef18836fd0fb34278dca6b3350bdcf1e8809822fc4f57a82e"}, + {file = "greenlet-3.0.0rc3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:256b748fc1e6c97012f217e0a403116cb0dd369bf1cff51c07a9c52899d4a8a8"}, + {file = "greenlet-3.0.0rc3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4936e6e051932848c4b237a874da8dbb47bfbb5ae5104497fb78c4f4cf184989"}, + {file = "greenlet-3.0.0rc3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a31b3a4bd10c540a7eb7d4b43d16779813ca4c79b615ed6d4ebf0e5a782d9fa0"}, + {file = "greenlet-3.0.0rc3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6f8253fdb00e74b928ab5d04f88ddbc8beb0cc26aa978bb4a12c1513166d481"}, + {file = "greenlet-3.0.0rc3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a7831d04a0f8a14645c010e3fb3fa36b8d2df304dd837948427ccfec2524ddf"}, + {file = "greenlet-3.0.0rc3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae84d2f2658990f29df4ea753061b25c337bd70f805128af328098e5b8afc454"}, + {file = "greenlet-3.0.0rc3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cd51cc2528a2985f3bc0564c1b1ce5b2e6fa4ee9924503010428256fa95b0e3c"}, + {file = "greenlet-3.0.0rc3-cp39-cp39-win32.whl", hash = "sha256:e8698f341e78dd0f149511929e92d1507cc26647f047db13987169d244db10fb"}, + {file = "greenlet-3.0.0rc3-cp39-cp39-win_amd64.whl", hash = "sha256:f059457db4e2ae4a4fdae455453c5e5765aa08efcb804e2a106c69c31bd438ba"}, + {file = "greenlet-3.0.0rc3-cp39-universal2-macosx_11_0_x86_64.whl", hash = "sha256:c80cac2776df3dd08f27b7338f467a62ee6cb29668a8f4f408b8da1f981aae9e"}, + {file = "greenlet-3.0.0rc3.tar.gz", hash = "sha256:0df5c2ad154f457fd372e39723493b3df519330a4c1bff3ca901be66130f379b"}, ] [package.extras] @@ -1348,29 +1395,15 @@ files = [ {file = "httpagentparser-1.9.5.tar.gz", hash = "sha256:53cefd9d65990f6fe59c0378cad8ea1b9df8f770d2e8bd9d8762edae033be80a"}, ] -[[package]] -name = "httplib2" -version = "0.22.0" -description = "A comprehensive HTTP client library." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, - {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, -] - -[package.dependencies] -pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} - [[package]] name = "humanize" -version = "4.7.0" +version = "4.8.0" description = "Python humanize utilities" optional = false python-versions = ">=3.8" files = [ - {file = "humanize-4.7.0-py3-none-any.whl", hash = "sha256:df7c429c2d27372b249d3f26eb53b07b166b661326e0325793e0a988082e3889"}, - {file = "humanize-4.7.0.tar.gz", hash = "sha256:7ca0e43e870981fa684acb5b062deb307218193bca1a01f2b2676479df849b3a"}, + {file = "humanize-4.8.0-py3-none-any.whl", hash = "sha256:8bc9e2bb9315e61ec06bf690151ae35aeb65651ab091266941edf97c90836404"}, + {file = "humanize-4.8.0.tar.gz", hash = "sha256:9783373bf1eec713a770ecaa7c2d7a7902c98398009dfa3d8a2df91eec9311e8"}, ] [package.extras] @@ -1442,13 +1475,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "kombu" -version = "5.3.1" +version = "5.3.2" description = "Messaging library for Python." optional = false python-versions = ">=3.8" files = [ - {file = "kombu-5.3.1-py3-none-any.whl", hash = "sha256:48ee589e8833126fd01ceaa08f8a2041334e9f5894e5763c8486a550454551e9"}, - {file = "kombu-5.3.1.tar.gz", hash = "sha256:fbd7572d92c0bf71c112a6b45163153dea5a7b6a701ec16b568c27d0fd2370f2"}, + {file = "kombu-5.3.2-py3-none-any.whl", hash = "sha256:b753c9cfc9b1e976e637a7cbc1a65d446a22e45546cd996ea28f932082b7dc9e"}, + {file = "kombu-5.3.2.tar.gz", hash = "sha256:0ba213f630a2cb2772728aef56ac6883dc3a2f13435e10048f6e97d48506dbbd"}, ] [package.dependencies] @@ -2007,37 +2040,38 @@ files = [ [[package]] name = "mypy" -version = "1.4.1" +version = "1.5.1" description = "Optional static typing for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, - {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, - {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, - {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, - {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, - {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, - {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, - {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, - {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, - {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, - {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, - {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, - {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, - {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, - {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, - {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, - {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, - {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, - {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, - {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, - {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, - {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, - {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, - {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, - {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, - {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"}, + {file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"}, + {file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"}, + {file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"}, + {file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"}, + {file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"}, + {file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"}, + {file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"}, + {file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"}, + {file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"}, + {file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"}, + {file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"}, + {file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"}, + {file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"}, + {file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"}, + {file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"}, + {file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"}, + {file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"}, ] [package.dependencies] @@ -2047,7 +2081,6 @@ typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] [[package]] @@ -2095,24 +2128,6 @@ files = [ {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, ] -[[package]] -name = "oauth2client" -version = "4.1.3" -description = "OAuth 2.0 client library" -optional = false -python-versions = "*" -files = [ - {file = "oauth2client-4.1.3-py2.py3-none-any.whl", hash = "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac"}, - {file = "oauth2client-4.1.3.tar.gz", hash = "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6"}, -] - -[package.dependencies] -httplib2 = ">=0.9.1" -pyasn1 = ">=0.1.7" -pyasn1-modules = ">=0.0.5" -rsa = ">=3.1.4" -six = ">=1.6.1" - [[package]] name = "ordered-set" version = "4.1.0" @@ -2165,76 +2180,76 @@ files = [ [[package]] name = "pathspec" -version = "0.11.1" +version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.7" files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] [[package]] name = "pillow" -version = "10.0.0" +version = "10.0.1" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "Pillow-10.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891"}, - {file = "Pillow-10.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1"}, - {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf"}, - {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3"}, - {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992"}, - {file = "Pillow-10.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de"}, - {file = "Pillow-10.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485"}, - {file = "Pillow-10.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd"}, - {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629"}, - {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"}, - {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"}, - {file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"}, - {file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"}, - {file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223"}, - {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff"}, - {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"}, - {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"}, - {file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"}, - {file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"}, - {file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530"}, - {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51"}, - {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86"}, - {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7"}, - {file = "Pillow-10.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0"}, - {file = "Pillow-10.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa"}, - {file = "Pillow-10.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3"}, - {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba"}, - {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3"}, - {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017"}, - {file = "Pillow-10.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3"}, - {file = "Pillow-10.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684"}, - {file = "Pillow-10.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3"}, - {file = "Pillow-10.0.0.tar.gz", hash = "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"}, + {file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"}, + {file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"}, + {file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"}, + {file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"}, + {file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"}, + {file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"}, + {file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"}, + {file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"}, + {file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"}, + {file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"}, + {file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"}, + {file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"}, + {file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"}, + {file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"}, + {file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"}, + {file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"}, + {file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"}, + {file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"}, + {file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"}, + {file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"}, + {file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"}, + {file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"}, + {file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"}, ] [package.extras] @@ -2243,18 +2258,18 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "platformdirs" -version = "3.9.1" +version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, - {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "prompt-toolkit" @@ -2282,84 +2297,82 @@ files = [ [[package]] name = "psycopg2-binary" -version = "2.9.6" +version = "2.9.7" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.6" files = [ - {file = "psycopg2-binary-2.9.6.tar.gz", hash = "sha256:1f64dcfb8f6e0c014c7f55e51c9759f024f70ea572fbdef123f85318c297947c"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d26e0342183c762de3276cca7a530d574d4e25121ca7d6e4a98e4f05cb8e4df7"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c48d8f2db17f27d41fb0e2ecd703ea41984ee19362cbce52c097963b3a1b4365"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe9dc0a884a8848075e576c1de0290d85a533a9f6e9c4e564f19adf8f6e54a7"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a76e027f87753f9bd1ab5f7c9cb8c7628d1077ef927f5e2446477153a602f2c"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6460c7a99fc939b849431f1e73e013d54aa54293f30f1109019c56a0b2b2ec2f"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae102a98c547ee2288637af07393dd33f440c25e5cd79556b04e3fca13325e5f"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9972aad21f965599ed0106f65334230ce826e5ae69fda7cbd688d24fa922415e"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7a40c00dbe17c0af5bdd55aafd6ff6679f94a9be9513a4c7e071baf3d7d22a70"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:cacbdc5839bdff804dfebc058fe25684cae322987f7a38b0168bc1b2df703fb1"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7f0438fa20fb6c7e202863e0d5ab02c246d35efb1d164e052f2f3bfe2b152bd0"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-win32.whl", hash = "sha256:b6c8288bb8a84b47e07013bb4850f50538aa913d487579e1921724631d02ea1b"}, - {file = "psycopg2_binary-2.9.6-cp310-cp310-win_amd64.whl", hash = "sha256:61b047a0537bbc3afae10f134dc6393823882eb263088c271331602b672e52e9"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:964b4dfb7c1c1965ac4c1978b0f755cc4bd698e8aa2b7667c575fb5f04ebe06b"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afe64e9b8ea66866a771996f6ff14447e8082ea26e675a295ad3bdbffdd72afb"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15e2ee79e7cf29582ef770de7dab3d286431b01c3bb598f8e05e09601b890081"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa74c903a3c1f0d9b1c7e7b53ed2d929a4910e272add6700c38f365a6002820"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b83456c2d4979e08ff56180a76429263ea254c3f6552cd14ada95cff1dec9bb8"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0645376d399bfd64da57148694d78e1f431b1e1ee1054872a5713125681cf1be"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99e34c82309dd78959ba3c1590975b5d3c862d6f279f843d47d26ff89d7d7e1"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4ea29fc3ad9d91162c52b578f211ff1c931d8a38e1f58e684c45aa470adf19e2"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4ac30da8b4f57187dbf449294d23b808f8f53cad6b1fc3623fa8a6c11d176dd0"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e78e6e2a00c223e164c417628572a90093c031ed724492c763721c2e0bc2a8df"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-win32.whl", hash = "sha256:1876843d8e31c89c399e31b97d4b9725a3575bb9c2af92038464231ec40f9edb"}, - {file = "psycopg2_binary-2.9.6-cp311-cp311-win_amd64.whl", hash = "sha256:b4b24f75d16a89cc6b4cdff0eb6a910a966ecd476d1e73f7ce5985ff1328e9a6"}, - {file = "psycopg2_binary-2.9.6-cp36-cp36m-win32.whl", hash = "sha256:498807b927ca2510baea1b05cc91d7da4718a0f53cb766c154c417a39f1820a0"}, - {file = "psycopg2_binary-2.9.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0d236c2825fa656a2d98bbb0e52370a2e852e5a0ec45fc4f402977313329174d"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:34b9ccdf210cbbb1303c7c4db2905fa0319391bd5904d32689e6dd5c963d2ea8"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d2222e61f313c4848ff05353653bf5f5cf6ce34df540e4274516880d9c3763"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30637a20623e2a2eacc420059be11527f4458ef54352d870b8181a4c3020ae6b"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8122cfc7cae0da9a3077216528b8bb3629c43b25053284cc868744bfe71eb141"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38601cbbfe600362c43714482f43b7c110b20cb0f8172422c616b09b85a750c5"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c7e62ab8b332147a7593a385d4f368874d5fe4ad4e341770d4983442d89603e3"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2ab652e729ff4ad76d400df2624d223d6e265ef81bb8aa17fbd63607878ecbee"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c83a74b68270028dc8ee74d38ecfaf9c90eed23c8959fca95bd703d25b82c88e"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d4e6036decf4b72d6425d5b29bbd3e8f0ff1059cda7ac7b96d6ac5ed34ffbacd"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-win32.whl", hash = "sha256:a8c28fd40a4226b4a84bdf2d2b5b37d2c7bd49486b5adcc200e8c7ec991dfa7e"}, - {file = "psycopg2_binary-2.9.6-cp37-cp37m-win_amd64.whl", hash = "sha256:51537e3d299be0db9137b321dfb6a5022caaab275775680e0c3d281feefaca6b"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4499e0a83b7b7edcb8dabecbd8501d0d3a5ef66457200f77bde3d210d5debb"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7e13a5a2c01151f1208d5207e42f33ba86d561b7a89fca67c700b9486a06d0e2"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e0f754d27fddcfd74006455b6e04e6705d6c31a612ec69ddc040a5468e44b4e"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d57c3fd55d9058645d26ae37d76e61156a27722097229d32a9e73ed54819982a"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71f14375d6f73b62800530b581aed3ada394039877818b2d5f7fc77e3bb6894d"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:441cc2f8869a4f0f4bb408475e5ae0ee1f3b55b33f350406150277f7f35384fc"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:65bee1e49fa6f9cf327ce0e01c4c10f39165ee76d35c846ade7cb0ec6683e303"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:af335bac6b666cc6aea16f11d486c3b794029d9df029967f9938a4bed59b6a19"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cfec476887aa231b8548ece2e06d28edc87c1397ebd83922299af2e051cf2827"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65c07febd1936d63bfde78948b76cd4c2a411572a44ac50719ead41947d0f26b"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-win32.whl", hash = "sha256:4dfb4be774c4436a4526d0c554af0cc2e02082c38303852a36f6456ece7b3503"}, - {file = "psycopg2_binary-2.9.6-cp38-cp38-win_amd64.whl", hash = "sha256:02c6e3cf3439e213e4ee930308dc122d6fb4d4bea9aef4a12535fbd605d1a2fe"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9182eb20f41417ea1dd8e8f7888c4d7c6e805f8a7c98c1081778a3da2bee3e4"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a6979cf527e2603d349a91060f428bcb135aea2be3201dff794813256c274f1"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8338a271cb71d8da40b023a35d9c1e919eba6cbd8fa20a54b748a332c355d896"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3ed340d2b858d6e6fb5083f87c09996506af483227735de6964a6100b4e6a54"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f81e65376e52f03422e1fb475c9514185669943798ed019ac50410fb4c4df232"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb13af3c5dd3a9588000910178de17010ebcccd37b4f9794b00595e3a8ddad3"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4c727b597c6444a16e9119386b59388f8a424223302d0c06c676ec8b4bc1f963"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4d67fbdaf177da06374473ef6f7ed8cc0a9dc640b01abfe9e8a2ccb1b1402c1f"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0892ef645c2fabb0c75ec32d79f4252542d0caec1d5d949630e7d242ca4681a3"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:02c0f3757a4300cf379eb49f543fb7ac527fb00144d39246ee40e1df684ab514"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-win32.whl", hash = "sha256:c3dba7dab16709a33a847e5cd756767271697041fbe3fe97c215b1fc1f5c9848"}, - {file = "psycopg2_binary-2.9.6-cp39-cp39-win_amd64.whl", hash = "sha256:f6a88f384335bb27812293fdb11ac6aee2ca3f51d3c7820fe03de0a304ab6249"}, + {file = "psycopg2-binary-2.9.7.tar.gz", hash = "sha256:1b918f64a51ffe19cd2e230b3240ba481330ce1d4b7875ae67305bd1d37b041c"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ea5f8ee87f1eddc818fc04649d952c526db4426d26bab16efbe5a0c52b27d6ab"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2993ccb2b7e80844d534e55e0f12534c2871952f78e0da33c35e648bf002bbff"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbbc3c5d15ed76b0d9db7753c0db40899136ecfe97d50cbde918f630c5eb857a"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:692df8763b71d42eb8343f54091368f6f6c9cfc56dc391858cdb3c3ef1e3e584"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dcfd5d37e027ec393a303cc0a216be564b96c80ba532f3d1e0d2b5e5e4b1e6e"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17cc17a70dfb295a240db7f65b6d8153c3d81efb145d76da1e4a096e9c5c0e63"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e5666632ba2b0d9757b38fc17337d84bdf932d38563c5234f5f8c54fd01349c9"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7db7b9b701974c96a88997d458b38ccb110eba8f805d4b4f74944aac48639b42"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c82986635a16fb1fa15cd5436035c88bc65c3d5ced1cfaac7f357ee9e9deddd4"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4fe13712357d802080cfccbf8c6266a3121dc0e27e2144819029095ccf708372"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-win32.whl", hash = "sha256:122641b7fab18ef76b18860dd0c772290566b6fb30cc08e923ad73d17461dc63"}, + {file = "psycopg2_binary-2.9.7-cp310-cp310-win_amd64.whl", hash = "sha256:f8651cf1f144f9ee0fa7d1a1df61a9184ab72962531ca99f077bbdcba3947c58"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ecc15666f16f97709106d87284c136cdc82647e1c3f8392a672616aed3c7151"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fbb1184c7e9d28d67671992970718c05af5f77fc88e26fd7136613c4ece1f89"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7968fd20bd550431837656872c19575b687f3f6f98120046228e451e4064df"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:094af2e77a1976efd4956a031028774b827029729725e136514aae3cdf49b87b"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26484e913d472ecb6b45937ea55ce29c57c662066d222fb0fbdc1fab457f18c5"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f309b77a7c716e6ed9891b9b42953c3ff7d533dc548c1e33fddc73d2f5e21f9"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6d92e139ca388ccfe8c04aacc163756e55ba4c623c6ba13d5d1595ed97523e4b"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2df562bb2e4e00ee064779902d721223cfa9f8f58e7e52318c97d139cf7f012d"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4eec5d36dbcfc076caab61a2114c12094c0b7027d57e9e4387b634e8ab36fd44"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1011eeb0c51e5b9ea1016f0f45fa23aca63966a4c0afcf0340ccabe85a9f65bd"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-win32.whl", hash = "sha256:ded8e15f7550db9e75c60b3d9fcbc7737fea258a0f10032cdb7edc26c2a671fd"}, + {file = "psycopg2_binary-2.9.7-cp311-cp311-win_amd64.whl", hash = "sha256:8a136c8aaf6615653450817a7abe0fc01e4ea720ae41dfb2823eccae4b9062a3"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dec5a75a3a5d42b120e88e6ed3e3b37b46459202bb8e36cd67591b6e5feebc1"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc10da7e7df3380426521e8c1ed975d22df678639da2ed0ec3244c3dc2ab54c8"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee919b676da28f78f91b464fb3e12238bd7474483352a59c8a16c39dfc59f0c5"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb1c0e682138f9067a58fc3c9a9bf1c83d8e08cfbee380d858e63196466d5c86"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00d8db270afb76f48a499f7bb8fa70297e66da67288471ca873db88382850bf4"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b0c2b466b2f4d89ccc33784c4ebb1627989bd84a39b79092e560e937a11d4ac"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:51d1b42d44f4ffb93188f9b39e6d1c82aa758fdb8d9de65e1ddfe7a7d250d7ad"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:11abdbfc6f7f7dea4a524b5f4117369b0d757725798f1593796be6ece20266cb"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f02f4a72cc3ab2565c6d9720f0343cb840fb2dc01a2e9ecb8bc58ccf95dc5c06"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-win32.whl", hash = "sha256:81d5dd2dd9ab78d31a451e357315f201d976c131ca7d43870a0e8063b6b7a1ec"}, + {file = "psycopg2_binary-2.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:62cb6de84d7767164a87ca97e22e5e0a134856ebcb08f21b621c6125baf61f16"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59f7e9109a59dfa31efa022e94a244736ae401526682de504e87bd11ce870c22"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:95a7a747bdc3b010bb6a980f053233e7610276d55f3ca506afff4ad7749ab58a"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c721ee464e45ecf609ff8c0a555018764974114f671815a0a7152aedb9f3343"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4f37bbc6588d402980ffbd1f3338c871368fb4b1cfa091debe13c68bb3852b3"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac83ab05e25354dad798401babaa6daa9577462136ba215694865394840e31f8"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:024eaeb2a08c9a65cd5f94b31ace1ee3bb3f978cd4d079406aef85169ba01f08"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1c31c2606ac500dbd26381145684d87730a2fac9a62ebcfbaa2b119f8d6c19f4"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:42a62ef0e5abb55bf6ffb050eb2b0fcd767261fa3faf943a4267539168807522"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7952807f95c8eba6a8ccb14e00bf170bb700cafcec3924d565235dffc7dc4ae8"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e02bc4f2966475a7393bd0f098e1165d470d3fa816264054359ed4f10f6914ea"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-win32.whl", hash = "sha256:fdca0511458d26cf39b827a663d7d87db6f32b93efc22442a742035728603d5f"}, + {file = "psycopg2_binary-2.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:d0b16e5bb0ab78583f0ed7ab16378a0f8a89a27256bb5560402749dbe8a164d7"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6822c9c63308d650db201ba22fe6648bd6786ca6d14fdaf273b17e15608d0852"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f94cb12150d57ea433e3e02aabd072205648e86f1d5a0a692d60242f7809b15"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5ee89587696d808c9a00876065d725d4ae606f5f7853b961cdbc348b0f7c9a1"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad5ec10b53cbb57e9a2e77b67e4e4368df56b54d6b00cc86398578f1c635f329"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:642df77484b2dcaf87d4237792246d8068653f9e0f5c025e2c692fc56b0dda70"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6a8b575ac45af1eaccbbcdcf710ab984fd50af048fe130672377f78aaff6fc1"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f955aa50d7d5220fcb6e38f69ea126eafecd812d96aeed5d5f3597f33fad43bb"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ad26d4eeaa0d722b25814cce97335ecf1b707630258f14ac4d2ed3d1d8415265"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ced63c054bdaf0298f62681d5dcae3afe60cbae332390bfb1acf0e23dcd25fc8"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b04da24cbde33292ad34a40db9832a80ad12de26486ffeda883413c9e1b1d5e"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-win32.whl", hash = "sha256:18f12632ab516c47c1ac4841a78fddea6508a8284c7cf0f292cb1a523f2e2379"}, + {file = "psycopg2_binary-2.9.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb3b8d55924a6058a26db69fb1d3e7e32695ff8b491835ba9f479537e14dcf9f"}, ] [[package]] name = "pyaml" -version = "23.7.0" +version = "23.9.6" description = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data" optional = false python-versions = ">=3.8" files = [ - {file = "pyaml-23.7.0-py3-none-any.whl", hash = "sha256:0a37018282545ccc31faecbe138fda4d89e236af04d691cfb5af00cd60089345"}, - {file = "pyaml-23.7.0.tar.gz", hash = "sha256:0c510bbb8938309400e0b1e47ac16fd90e56d652805a93417128786718f33546"}, + {file = "pyaml-23.9.6-py3-none-any.whl", hash = "sha256:9dcc67922b7278f3680e573324b2e8a8d2f86c5d09bf640cba83735fb1663e97"}, + {file = "pyaml-23.9.6.tar.gz", hash = "sha256:2b2c39017b718a127bef9f96bc55f89414d960876668d69880aae66f4ba98957"}, ] [package.dependencies] @@ -2368,40 +2381,15 @@ PyYAML = "*" [package.extras] anchors = ["unidecode"] -[[package]] -name = "pyasn1" -version = "0.5.0" -description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, - {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, -] - -[[package]] -name = "pyasn1-modules" -version = "0.3.0" -description = "A collection of ASN.1-based protocols modules" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, - {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, -] - -[package.dependencies] -pyasn1 = ">=0.4.6,<0.6.0" - [[package]] name = "pycodestyle" -version = "2.10.0" +version = "2.11.0" description = "Python style guide checker" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, - {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, + {file = "pycodestyle-2.11.0-py2.py3-none-any.whl", hash = "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8"}, + {file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"}, ] [[package]] @@ -2417,13 +2405,13 @@ files = [ [[package]] name = "pygments" -version = "2.15.1" +version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, - {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, ] [package.extras] @@ -2480,20 +2468,6 @@ files = [ {file = "pypandoc-1.11.tar.gz", hash = "sha256:7f6d68db0e57e0f6961bec2190897118c4d305fc2d31c22cd16037f22ee084a5"}, ] -[[package]] -name = "pyparsing" -version = "3.1.0" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.1.0-py3-none-any.whl", hash = "sha256:d554a96d1a7d3ddaf7183104485bc19fd80543ad6ac5bdb6426719d766fb06c1"}, - {file = "pyparsing-3.1.0.tar.gz", hash = "sha256:edb662d6fe322d6e990b1594b5feaeadf806803359e3d4d42f11e295e588f0ea"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pysaml2" version = "7.4.2" @@ -2585,13 +2559,13 @@ postgresql = ["psycopg2"] [[package]] name = "pytz" -version = "2023.3" +version = "2023.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] [[package]] @@ -2739,38 +2713,24 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "responses" -version = "0.23.2" +version = "0.23.3" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.7" files = [ - {file = "responses-0.23.2-py3-none-any.whl", hash = "sha256:9d49c218ba3079022bd63427f12b0a43b43d2f6aaf5ed859b9df9d733b4dd775"}, - {file = "responses-0.23.2.tar.gz", hash = "sha256:5d5a2ce3285f84e1f107d2e942476b6c7dff3747f289c0eae997cb77d2ab68e8"}, + {file = "responses-0.23.3-py3-none-any.whl", hash = "sha256:e6fbcf5d82172fecc0aa1860fd91e58cbfd96cee5e96da5b63fa6eb3caa10dd3"}, + {file = "responses-0.23.3.tar.gz", hash = "sha256:205029e1cb334c21cb4ec64fc7599be48b859a0fd381a42443cdd600bfe8b16a"}, ] [package.dependencies] pyyaml = "*" requests = ">=2.30.0,<3.0" types-PyYAML = "*" -urllib3 = ">=2.0.0,<3.0" +urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-requests"] -[[package]] -name = "rsa" -version = "4.9" -description = "Pure-Python RSA implementation" -optional = false -python-versions = ">=3.6,<4" -files = [ - {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, - {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, -] - -[package.dependencies] -pyasn1 = ">=0.1.3" - [[package]] name = "scipy" version = "1.9.3" @@ -2811,13 +2771,13 @@ test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "sciki [[package]] name = "selenium" -version = "4.10.0" +version = "4.12.0" description = "" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "selenium-4.10.0-py3-none-any.whl", hash = "sha256:40241b9d872f58959e9b34e258488bf11844cd86142fd68182bd41db9991fc5c"}, - {file = "selenium-4.10.0.tar.gz", hash = "sha256:871bf800c4934f745b909c8dfc7d15c65cf45bd2e943abd54451c810ada395e3"}, + {file = "selenium-4.12.0-py3-none-any.whl", hash = "sha256:b2c48b1440db54a0653300d9955f5421390723d53b36ec835e18de8e13bbd401"}, + {file = "selenium-4.12.0.tar.gz", hash = "sha256:95be6aa449a0ab4ac1198bb9de71bbe9170405e04b9752f4b450dc7292a21828"}, ] [package.dependencies] @@ -2828,19 +2788,19 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]} [[package]] name = "setuptools" -version = "68.0.0" +version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "simplejson" @@ -2982,24 +2942,24 @@ files = [ [[package]] name = "soupsieve" -version = "2.4.1" +version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, - {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] [[package]] name = "sphinx" -version = "7.1.0" +version = "7.2.6" description = "Python documentation generator" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinx-7.1.0-py3-none-any.whl", hash = "sha256:9bdfb5a2b28351d4fdf40a63cd006dbad727f793b243e669fc950d7116c634af"}, - {file = "sphinx-7.1.0.tar.gz", hash = "sha256:8f336d0221c3beb23006b3164ed1d46db9cebcce9cb41cdb9c5ecd4bcc509be0"}, + {file = "sphinx-7.2.6-py3-none-any.whl", hash = "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560"}, + {file = "sphinx-7.2.6.tar.gz", hash = "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5"}, ] [package.dependencies] @@ -3010,7 +2970,7 @@ docutils = ">=0.18.1,<0.21" imagesize = ">=1.3" Jinja2 = ">=3.0" packaging = ">=21.0" -Pygments = ">=2.13" +Pygments = ">=2.14" requests = ">=2.25.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" @@ -3018,54 +2978,63 @@ sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" +sphinxcontrib-serializinghtml = ">=1.1.9" [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] +test = ["cython (>=3.0)", "filelock", "html5lib", "pytest (>=4.6)", "setuptools (>=67.0)"] [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.4" +version = "1.0.7" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, + {file = "sphinxcontrib_applehelp-1.0.7-py3-none-any.whl", hash = "sha256:094c4d56209d1734e7d252f6e0b3ccc090bd52ee56807a5d9315b19c122ab15d"}, + {file = "sphinxcontrib_applehelp-1.0.7.tar.gz", hash = "sha256:39fdc8d762d33b01a7d8f026a3b7d71563ea3b72787d5f00ad8465bd9d6dfbfa"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +version = "1.0.5" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, + {file = "sphinxcontrib_devhelp-1.0.5-py3-none-any.whl", hash = "sha256:fe8009aed765188f08fcaadbb3ea0d90ce8ae2d76710b7e29ea7d047177dae2f"}, + {file = "sphinxcontrib_devhelp-1.0.5.tar.gz", hash = "sha256:63b41e0d38207ca40ebbeabcf4d8e51f76c03e78cd61abe118cf4435c73d4212"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.1" +version = "2.0.4" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, + {file = "sphinxcontrib_htmlhelp-2.0.4-py3-none-any.whl", hash = "sha256:8001661c077a73c29beaf4a79968d0726103c5605e27db92b9ebed8bab1359e9"}, + {file = "sphinxcontrib_htmlhelp-2.0.4.tar.gz", hash = "sha256:6c26a118a05b76000738429b724a0568dbde5b72391a688577da08f11891092a"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] @@ -3086,82 +3055,88 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +version = "1.0.6" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, + {file = "sphinxcontrib_qthelp-1.0.6-py3-none-any.whl", hash = "sha256:bf76886ee7470b934e363da7a954ea2825650013d367728588732c7350f49ea4"}, + {file = "sphinxcontrib_qthelp-1.0.6.tar.gz", hash = "sha256:62b9d1a186ab7f5ee3356d906f648cacb7a6bdb94d201ee7adf26db55092982d"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +version = "1.1.9" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, + {file = "sphinxcontrib_serializinghtml-1.1.9-py3-none-any.whl", hash = "sha256:9b36e503703ff04f20e9675771df105e58aa029cfcbc23b8ed716019b7416ae1"}, + {file = "sphinxcontrib_serializinghtml-1.1.9.tar.gz", hash = "sha256:0c64ff898339e1fac29abd2bf5f11078f3ec413cfe9c046d3120d7ca65530b54"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sqlalchemy" -version = "2.0.19" +version = "2.0.20" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9deaae357edc2091a9ed5d25e9ee8bba98bcfae454b3911adeaf159c2e9ca9e3"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bf0fd65b50a330261ec7fe3d091dfc1c577483c96a9fa1e4323e932961aa1b5"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d90ccc15ba1baa345796a8fb1965223ca7ded2d235ccbef80a47b85cea2d71a"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4e688f6784427e5f9479d1a13617f573de8f7d4aa713ba82813bcd16e259d1"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:584f66e5e1979a7a00f4935015840be627e31ca29ad13f49a6e51e97a3fb8cae"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c69ce70047b801d2aba3e5ff3cba32014558966109fecab0c39d16c18510f15"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-win32.whl", hash = "sha256:96f0463573469579d32ad0c91929548d78314ef95c210a8115346271beeeaaa2"}, - {file = "SQLAlchemy-2.0.19-cp310-cp310-win_amd64.whl", hash = "sha256:22bafb1da60c24514c141a7ff852b52f9f573fb933b1e6b5263f0daa28ce6db9"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6894708eeb81f6d8193e996257223b6bb4041cb05a17cd5cf373ed836ef87a2"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8f2afd1aafded7362b397581772c670f20ea84d0a780b93a1a1529da7c3d369"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15afbf5aa76f2241184c1d3b61af1a72ba31ce4161013d7cb5c4c2fca04fd6e"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc05b59142445a4efb9c1fd75c334b431d35c304b0e33f4fa0ff1ea4890f92e"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5831138f0cc06b43edf5f99541c64adf0ab0d41f9a4471fd63b54ae18399e4de"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3afa8a21a9046917b3a12ffe016ba7ebe7a55a6fc0c7d950beb303c735c3c3ad"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-win32.whl", hash = "sha256:c896d4e6ab2eba2afa1d56be3d0b936c56d4666e789bfc59d6ae76e9fcf46145"}, - {file = "SQLAlchemy-2.0.19-cp311-cp311-win_amd64.whl", hash = "sha256:024d2f67fb3ec697555e48caeb7147cfe2c08065a4f1a52d93c3d44fc8e6ad1c"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:89bc2b374ebee1a02fd2eae6fd0570b5ad897ee514e0f84c5c137c942772aa0c"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd4d410a76c3762511ae075d50f379ae09551d92525aa5bb307f8343bf7c2c12"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f469f15068cd8351826df4080ffe4cc6377c5bf7d29b5a07b0e717dddb4c7ea2"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cda283700c984e699e8ef0fcc5c61f00c9d14b6f65a4f2767c97242513fcdd84"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:43699eb3f80920cc39a380c159ae21c8a8924fe071bccb68fc509e099420b148"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-win32.whl", hash = "sha256:61ada5831db36d897e28eb95f0f81814525e0d7927fb51145526c4e63174920b"}, - {file = "SQLAlchemy-2.0.19-cp37-cp37m-win_amd64.whl", hash = "sha256:57d100a421d9ab4874f51285c059003292433c648df6abe6c9c904e5bd5b0828"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16a310f5bc75a5b2ce7cb656d0e76eb13440b8354f927ff15cbaddd2523ee2d1"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf7b5e3856cbf1876da4e9d9715546fa26b6e0ba1a682d5ed2fc3ca4c7c3ec5b"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e7b69d9ced4b53310a87117824b23c509c6fc1f692aa7272d47561347e133b6"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9eb4575bfa5afc4b066528302bf12083da3175f71b64a43a7c0badda2be365"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6b54d1ad7a162857bb7c8ef689049c7cd9eae2f38864fc096d62ae10bc100c7d"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5d6afc41ca0ecf373366fd8e10aee2797128d3ae45eb8467b19da4899bcd1ee0"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-win32.whl", hash = "sha256:430614f18443b58ceb9dedec323ecddc0abb2b34e79d03503b5a7579cd73a531"}, - {file = "SQLAlchemy-2.0.19-cp38-cp38-win_amd64.whl", hash = "sha256:eb60699de43ba1a1f77363f563bb2c652f7748127ba3a774f7cf2c7804aa0d3d"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a752b7a9aceb0ba173955d4f780c64ee15a1a991f1c52d307d6215c6c73b3a4c"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7351c05db355da112e056a7b731253cbeffab9dfdb3be1e895368513c7d70106"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa51ce4aea583b0c6b426f4b0563d3535c1c75986c4373a0987d84d22376585b"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae7473a67cd82a41decfea58c0eac581209a0aa30f8bc9190926fbf628bb17f7"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:851a37898a8a39783aab603c7348eb5b20d83c76a14766a43f56e6ad422d1ec8"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539010665c90e60c4a1650afe4ab49ca100c74e6aef882466f1de6471d414be7"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-win32.whl", hash = "sha256:f82c310ddf97b04e1392c33cf9a70909e0ae10a7e2ddc1d64495e3abdc5d19fb"}, - {file = "SQLAlchemy-2.0.19-cp39-cp39-win_amd64.whl", hash = "sha256:8e712cfd2e07b801bc6b60fdf64853bc2bd0af33ca8fa46166a23fe11ce0dbb0"}, - {file = "SQLAlchemy-2.0.19-py3-none-any.whl", hash = "sha256:314145c1389b021a9ad5aa3a18bac6f5d939f9087d7fc5443be28cba19d2c972"}, - {file = "SQLAlchemy-2.0.19.tar.gz", hash = "sha256:77a14fa20264af73ddcdb1e2b9c5a829b8cc6b8304d0f093271980e36c200a3f"}, + {file = "SQLAlchemy-2.0.20-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759b51346aa388c2e606ee206c0bc6f15a5299f6174d1e10cadbe4530d3c7a98"}, + {file = "SQLAlchemy-2.0.20-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1506e988ebeaaf316f183da601f24eedd7452e163010ea63dbe52dc91c7fc70e"}, + {file = "SQLAlchemy-2.0.20-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5768c268df78bacbde166b48be788b83dddaa2a5974b8810af422ddfe68a9bc8"}, + {file = "SQLAlchemy-2.0.20-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3f0dd6d15b6dc8b28a838a5c48ced7455c3e1fb47b89da9c79cc2090b072a50"}, + {file = "SQLAlchemy-2.0.20-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:243d0fb261f80a26774829bc2cee71df3222587ac789b7eaf6555c5b15651eed"}, + {file = "SQLAlchemy-2.0.20-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb6d77c31e1bf4268b4d61b549c341cbff9842f8e115ba6904249c20cb78a61"}, + {file = "SQLAlchemy-2.0.20-cp310-cp310-win32.whl", hash = "sha256:bcb04441f370cbe6e37c2b8d79e4af9e4789f626c595899d94abebe8b38f9a4d"}, + {file = "SQLAlchemy-2.0.20-cp310-cp310-win_amd64.whl", hash = "sha256:d32b5ffef6c5bcb452723a496bad2d4c52b346240c59b3e6dba279f6dcc06c14"}, + {file = "SQLAlchemy-2.0.20-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd81466bdbc82b060c3c110b2937ab65ace41dfa7b18681fdfad2f37f27acdd7"}, + {file = "SQLAlchemy-2.0.20-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6fe7d61dc71119e21ddb0094ee994418c12f68c61b3d263ebaae50ea8399c4d4"}, + {file = "SQLAlchemy-2.0.20-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4e571af672e1bb710b3cc1a9794b55bce1eae5aed41a608c0401885e3491179"}, + {file = "SQLAlchemy-2.0.20-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3364b7066b3c7f4437dd345d47271f1251e0cfb0aba67e785343cdbdb0fff08c"}, + {file = "SQLAlchemy-2.0.20-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1be86ccea0c965a1e8cd6ccf6884b924c319fcc85765f16c69f1ae7148eba64b"}, + {file = "SQLAlchemy-2.0.20-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1d35d49a972649b5080557c603110620a86aa11db350d7a7cb0f0a3f611948a0"}, + {file = "SQLAlchemy-2.0.20-cp311-cp311-win32.whl", hash = "sha256:27d554ef5d12501898d88d255c54eef8414576f34672e02fe96d75908993cf53"}, + {file = "SQLAlchemy-2.0.20-cp311-cp311-win_amd64.whl", hash = "sha256:411e7f140200c02c4b953b3dbd08351c9f9818d2bd591b56d0fa0716bd014f1e"}, + {file = "SQLAlchemy-2.0.20-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3c6aceebbc47db04f2d779db03afeaa2c73ea3f8dcd3987eb9efdb987ffa09a3"}, + {file = "SQLAlchemy-2.0.20-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d3f175410a6db0ad96b10bfbb0a5530ecd4fcf1e2b5d83d968dd64791f810ed"}, + {file = "SQLAlchemy-2.0.20-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8186be85da6587456c9ddc7bf480ebad1a0e6dcbad3967c4821233a4d4df57"}, + {file = "SQLAlchemy-2.0.20-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c3d99ba99007dab8233f635c32b5cd24fb1df8d64e17bc7df136cedbea427897"}, + {file = "SQLAlchemy-2.0.20-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:76fdfc0f6f5341987474ff48e7a66c3cd2b8a71ddda01fa82fedb180b961630a"}, + {file = "SQLAlchemy-2.0.20-cp37-cp37m-win32.whl", hash = "sha256:d3793dcf5bc4d74ae1e9db15121250c2da476e1af8e45a1d9a52b1513a393459"}, + {file = "SQLAlchemy-2.0.20-cp37-cp37m-win_amd64.whl", hash = "sha256:79fde625a0a55220d3624e64101ed68a059c1c1f126c74f08a42097a72ff66a9"}, + {file = "SQLAlchemy-2.0.20-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:599ccd23a7146e126be1c7632d1d47847fa9f333104d03325c4e15440fc7d927"}, + {file = "SQLAlchemy-2.0.20-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1a58052b5a93425f656675673ef1f7e005a3b72e3f2c91b8acca1b27ccadf5f4"}, + {file = "SQLAlchemy-2.0.20-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79543f945be7a5ada9943d555cf9b1531cfea49241809dd1183701f94a748624"}, + {file = "SQLAlchemy-2.0.20-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63e73da7fb030ae0a46a9ffbeef7e892f5def4baf8064786d040d45c1d6d1dc5"}, + {file = "SQLAlchemy-2.0.20-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ce5e81b800a8afc870bb8e0a275d81957e16f8c4b62415a7b386f29a0cb9763"}, + {file = "SQLAlchemy-2.0.20-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb0d3e94c2a84215532d9bcf10229476ffd3b08f481c53754113b794afb62d14"}, + {file = "SQLAlchemy-2.0.20-cp38-cp38-win32.whl", hash = "sha256:8dd77fd6648b677d7742d2c3cc105a66e2681cc5e5fb247b88c7a7b78351cf74"}, + {file = "SQLAlchemy-2.0.20-cp38-cp38-win_amd64.whl", hash = "sha256:6f8a934f9dfdf762c844e5164046a9cea25fabbc9ec865c023fe7f300f11ca4a"}, + {file = "SQLAlchemy-2.0.20-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:26a3399eaf65e9ab2690c07bd5cf898b639e76903e0abad096cd609233ce5208"}, + {file = "SQLAlchemy-2.0.20-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4cde2e1096cbb3e62002efdb7050113aa5f01718035ba9f29f9d89c3758e7e4e"}, + {file = "SQLAlchemy-2.0.20-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b09ba72e4e6d341bb5bdd3564f1cea6095d4c3632e45dc69375a1dbe4e26ec"}, + {file = "SQLAlchemy-2.0.20-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b74eeafaa11372627ce94e4dc88a6751b2b4d263015b3523e2b1e57291102f0"}, + {file = "SQLAlchemy-2.0.20-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:77d37c1b4e64c926fa3de23e8244b964aab92963d0f74d98cbc0783a9e04f501"}, + {file = "SQLAlchemy-2.0.20-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:eefebcc5c555803065128401a1e224a64607259b5eb907021bf9b175f315d2a6"}, + {file = "SQLAlchemy-2.0.20-cp39-cp39-win32.whl", hash = "sha256:3423dc2a3b94125094897118b52bdf4d37daf142cbcf26d48af284b763ab90e9"}, + {file = "SQLAlchemy-2.0.20-cp39-cp39-win_amd64.whl", hash = "sha256:5ed61e3463021763b853628aef8bc5d469fe12d95f82c74ef605049d810f3267"}, + {file = "SQLAlchemy-2.0.20-py3-none-any.whl", hash = "sha256:63a368231c53c93e2b67d0c5556a9836fdcd383f7e3026a39602aad775b14acf"}, + {file = "SQLAlchemy-2.0.20.tar.gz", hash = "sha256:ca8a5ff2aa7f3ade6c498aaafce25b1eaeabe4e42b73e25519183e4566a16fc6"}, ] [package.dependencies] @@ -3169,7 +3144,7 @@ greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or pl typing-extensions = ">=4.2.0" [package.extras] -aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] @@ -3241,13 +3216,13 @@ sortedcontainers = "*" [[package]] name = "trio-websocket" -version = "0.10.3" +version = "0.10.4" description = "WebSocket library for Trio" optional = false python-versions = ">=3.7" files = [ - {file = "trio-websocket-0.10.3.tar.gz", hash = "sha256:1a748604ad906a7dcab9a43c6eb5681e37de4793ba0847ef0bc9486933ed027b"}, - {file = "trio_websocket-0.10.3-py3-none-any.whl", hash = "sha256:a9937d48e8132ebf833019efde2a52ca82d223a30a7ea3e8d60a7d28f75a4e3a"}, + {file = "trio-websocket-0.10.4.tar.gz", hash = "sha256:e66b3db3e2453017431dfbd352081006654e1241c2a6800dc2f43d7df54d55c5"}, + {file = "trio_websocket-0.10.4-py3-none-any.whl", hash = "sha256:c7a620c4013c34b7e4477d89fe76695da1e455e4510a8d7ae13f81c632bdce1d"}, ] [package.dependencies] @@ -3328,13 +3303,13 @@ files = [ [[package]] name = "types-pytz" -version = "2023.3.0.0" +version = "2023.3.0.1" description = "Typing stubs for pytz" optional = false python-versions = "*" files = [ - {file = "types-pytz-2023.3.0.0.tar.gz", hash = "sha256:ecdc70d543aaf3616a7e48631543a884f74205f284cefd6649ddf44c6a820aac"}, - {file = "types_pytz-2023.3.0.0-py3-none-any.whl", hash = "sha256:4fc2a7fbbc315f0b6630e0b899fd6c743705abe1094d007b0e612d10da15e0f3"}, + {file = "types-pytz-2023.3.0.1.tar.gz", hash = "sha256:1a7b8d4aac70981cfa24478a41eadfcd96a087c986d6f150d77e3ceb3c2bdfab"}, + {file = "types_pytz-2023.3.0.1-py3-none-any.whl", hash = "sha256:65152e872137926bb67a8fe6cc9cfd794365df86650c5d5fdc7b167b0f38892e"}, ] [[package]] @@ -3350,13 +3325,13 @@ files = [ [[package]] name = "types-redis" -version = "4.6.0.3" +version = "4.6.0.6" description = "Typing stubs for redis" optional = false python-versions = "*" files = [ - {file = "types-redis-4.6.0.3.tar.gz", hash = "sha256:efdef37dc0c04bf5786195651fd694f8bfdd693eac09ec4af46d90f72652558f"}, - {file = "types_redis-4.6.0.3-py3-none-any.whl", hash = "sha256:67c44c14369c33c2a300da2a50b5607c0fc888f7b85eeb7c73e15c78a0f05edd"}, + {file = "types-redis-4.6.0.6.tar.gz", hash = "sha256:7865a843802937ab2ddca33579c4e255bfe73f87af85824ead7a6729ba92fc52"}, + {file = "types_redis-4.6.0.6-py3-none-any.whl", hash = "sha256:e0e9dcc530623db3a41ec058ccefdcd5c7582557f02ab5f7aa9a27fe10a78d7e"}, ] [package.dependencies] @@ -3554,13 +3529,13 @@ files = [ [[package]] name = "werkzeug" -version = "2.3.6" +version = "2.3.7" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" files = [ - {file = "Werkzeug-2.3.6-py3-none-any.whl", hash = "sha256:935539fa1413afbb9195b24880778422ed620c0fc09670945185cce4d91a8890"}, - {file = "Werkzeug-2.3.6.tar.gz", hash = "sha256:98c774df2f91b05550078891dee5f0eb0cb797a522c757a2452b9cee5b202330"}, + {file = "werkzeug-2.3.7-py3-none-any.whl", hash = "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"}, + {file = "werkzeug-2.3.7.tar.gz", hash = "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8"}, ] [package.dependencies] @@ -3571,13 +3546,13 @@ watchdog = ["watchdog (>=2.3)"] [[package]] name = "wheel" -version = "0.41.0" +version = "0.41.2" description = "A built-package format for Python" optional = false python-versions = ">=3.7" files = [ - {file = "wheel-0.41.0-py3-none-any.whl", hash = "sha256:7e9be3bbd0078f6147d82ed9ed957e323e7708f57e134743d2edef3a7b7972a9"}, - {file = "wheel-0.41.0.tar.gz", hash = "sha256:55a0f0a5a84869bce5ba775abfd9c462e3a6b1b7b7ec69d72c0b83d673a5114d"}, + {file = "wheel-0.41.2-py3-none-any.whl", hash = "sha256:75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8"}, + {file = "wheel-0.41.2.tar.gz", hash = "sha256:0c5ac5ff2afb79ac23ab82bab027a0be7b5dbcf2e54dc50efe4bf507de1f7985"}, ] [package.extras] @@ -3616,22 +3591,22 @@ email = ["email-validator"] [[package]] name = "xmlschema" -version = "2.3.1" +version = "2.4.0" description = "An XML Schema validator and decoder" optional = false python-versions = ">=3.7" files = [ - {file = "xmlschema-2.3.1-py3-none-any.whl", hash = "sha256:eac0e10957723689ff0691785da4ffee1e95df3a874e685a179047f7bf07f8fb"}, - {file = "xmlschema-2.3.1.tar.gz", hash = "sha256:2eb426c5710833a05610c22c8766713a1b87e9405e3eca0b7c658375bf7ec810"}, + {file = "xmlschema-2.4.0-py3-none-any.whl", hash = "sha256:dc87be0caaa61f42649899189aab2fd8e0d567f2cf548433ba7b79278d231a4a"}, + {file = "xmlschema-2.4.0.tar.gz", hash = "sha256:d74cd0c10866ac609e1ef94a5a69b018ad16e39077bc6393408b40c6babee793"}, ] [package.dependencies] -elementpath = ">=4.1.2,<5.0.0" +elementpath = ">=4.1.5,<5.0.0" [package.extras] -codegen = ["elementpath (>=4.1.2,<5.0.0)", "jinja2"] -dev = ["Sphinx", "coverage", "elementpath (>=4.1.2,<5.0.0)", "flake8", "jinja2", "lxml", "lxml-stubs", "memory-profiler", "mypy", "sphinx-rtd-theme", "tox"] -docs = ["Sphinx", "elementpath (>=4.1.2,<5.0.0)", "jinja2", "sphinx-rtd-theme"] +codegen = ["elementpath (>=4.1.5,<5.0.0)", "jinja2"] +dev = ["Sphinx", "coverage", "elementpath (>=4.1.5,<5.0.0)", "flake8", "jinja2", "lxml", "lxml-stubs", "memory-profiler", "mypy", "sphinx-rtd-theme", "tox"] +docs = ["Sphinx", "elementpath (>=4.1.5,<5.0.0)", "jinja2", "sphinx-rtd-theme"] [[package]] name = "yarl" @@ -3788,4 +3763,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "af904fa16bb82d9728a7895c70f239f9cbf4c6062a4076e58040ed3599a6724b" +content-hash = "9edb4fcfbc53d1977315dc912e505c5164a49bcd0bd4826a1a284e02876fd0fa" diff --git a/pyproject.toml b/pyproject.toml index 0aa0402cb7..b108274a7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ Flask-Assets = "^2.0" Flask-Caching = "^2.0.2" Flask-Compress = "^1.12" Flask-Migrate = "^4.0.4" -flask-oidc = "^1.4.0" +flask-oidc = "^2.0.3" Flask-OpenID = "^1.3.0" Flask-SQLAlchemy = "^3.0.5" Flask-Testing = "^0.8.1" @@ -93,6 +93,7 @@ langcodes = "^3.3.0" black = {extras = ["d"], version = "^23.3.0"} types-pysaml2 = "^1.0.0" qulacs = "^0.6.1" +SQLAlchemy = "^2.0.19" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/timApp/Dockerfile b/timApp/Dockerfile index 0b983996a8..69034ebded 100755 --- a/timApp/Dockerfile +++ b/timApp/Dockerfile @@ -197,7 +197,7 @@ RUN /usr/local/bin/wrap_chrome_binary ARG CHROME_DRIVER_VERSION RUN if [ ! -z "$CHROME_DRIVER_VERSION" ]; \ then CHROME_DRIVER_URL=https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/$CHROME_DRIVER_VERSION/linux64/chromedriver-linux64.zip ; \ - else echo "Geting ChromeDriver binary from https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" \ + else echo "Getting ChromeDriver binary from https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json" \ && CFT_URL=https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json \ && CFT_CHANNEL="Stable" \ && if [ "$CHROME_VERSION" = "google-chrome-beta" ]; then CFT_CHANNEL="Beta" ; fi \ diff --git a/timApp/answer/answer.py b/timApp/answer/answer.py index dfbe51c585..2150a99a31 100644 --- a/timApp/answer/answer.py +++ b/timApp/answer/answer.py @@ -6,8 +6,8 @@ from timApp.answer.answer_models import UserAnswer, AnswerUpload from timApp.plugin.taskid import TaskId -from timApp.timdb.sqa import include_if_loaded -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import include_if_loaded, db +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.user.user import User @@ -15,7 +15,7 @@ from timApp.plugin.plugintype import PluginType -class AnswerSaver(DbModel): +class AnswerSaver(db.Model): """Holds information about who has saved an answer. For example, in teacher view, "Save teacher's fix" would store the teacher in this table. """ @@ -24,7 +24,7 @@ class AnswerSaver(DbModel): user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) -class Answer(DbModel): +class Answer(db.Model): """An answer to a task.""" id: Mapped[int] = mapped_column(primary_key=True) diff --git a/timApp/answer/answer_models.py b/timApp/answer/answer_models.py index 7b38efcaa8..8d8b36699a 100644 --- a/timApp/answer/answer_models.py +++ b/timApp/answer/answer_models.py @@ -3,14 +3,14 @@ from sqlalchemy import UniqueConstraint, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db if TYPE_CHECKING: from timApp.item.block import Block from timApp.answer.answer import Answer -class AnswerTag(DbModel): +class AnswerTag(db.Model): """Tags for an Answer. TODO: Answer should be a Block and the tags would then come from the tag table. @@ -21,7 +21,7 @@ class AnswerTag(DbModel): tag: Mapped[str] -class AnswerUpload(DbModel): +class AnswerUpload(db.Model): """Associates uploaded files (Block with type BlockType.AnswerUpload) with Answers.""" upload_block_id: Mapped[int] = mapped_column( @@ -37,7 +37,7 @@ def __init__(self, block, answer=None): self.answer = answer -class UserAnswer(DbModel): +class UserAnswer(db.Model): """Associates Users with Answers.""" id: Mapped[int] = mapped_column(primary_key=True) diff --git a/timApp/answer/answers.py b/timApp/answer/answers.py index af5ff4d7d9..b65db0b677 100644 --- a/timApp/answer/answers.py +++ b/timApp/answer/answers.py @@ -204,7 +204,7 @@ def save_answer( for tag in tags: at = AnswerTag(tag=tag) - a.tags.append(at) + db.session.add(at) if saver: a.saver = saver db.session.flush() diff --git a/timApp/auth/access/routes.py b/timApp/auth/access/routes.py index ce8e65c4fa..88827866ce 100644 --- a/timApp/auth/access/routes.py +++ b/timApp/auth/access/routes.py @@ -62,7 +62,7 @@ def lock_active_groups(group_ids: list[int] | None) -> Response: return ok_response() user = get_current_user_object() - user.bypass_access_lock = True + user.skip_access_lock = True group_ids_set = set(group_ids) group_ids_set -= set(ug.id for ug in user.groups) @@ -101,7 +101,7 @@ def show_edit_info(group_name: str) -> Response: """ verify_logged_in() user = get_current_user_object() - user.bypass_access_lock = True + user.skip_access_lock = True ug = get_group_or_abort(group_name) if not user.is_admin: verify_group_edit_access(ug) @@ -124,7 +124,7 @@ def find_editable_groups( """ verify_logged_in() user = get_current_user_object() - user.bypass_access_lock = True + user.skip_access_lock = True ugs = run_sql(select(UserGroup).filter(UserGroup.id.in_(group_ids))).scalars().all() visible_ugs = [ ug for ug in ugs if user.is_admin or verify_group_edit_access(ug, require=False) diff --git a/timApp/auth/auth_models.py b/timApp/auth/auth_models.py index b10d97f4de..c143a7f2d5 100644 --- a/timApp/auth/auth_models.py +++ b/timApp/auth/auth_models.py @@ -7,18 +7,18 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.auth.accesstype import AccessType -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.item.distribute_rights import Right from timApp.item.block import Block from timApp.user.usergroup import UserGroup -from timApp.timdb.sqa import include_if_loaded +from timApp.timdb.sqa import include_if_loaded, db from timApp.util.utils import get_current_time -class AccessTypeModel(DbModel): +class AccessTypeModel(db.Model): """A kind of access that a UserGroup may have to a Block.""" __tablename__ = "accesstype" @@ -38,7 +38,7 @@ def to_enum(self): return AccessType(self.id) -class BlockAccess(DbModel): +class BlockAccess(db.Model): """A single permission. Relates a UserGroup with a Block along with an AccessType.""" block_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) diff --git a/timApp/auth/oauth2/models.py b/timApp/auth/oauth2/models.py index 84788b2095..4bc7a64179 100644 --- a/timApp/auth/oauth2/models.py +++ b/timApp/auth/oauth2/models.py @@ -13,7 +13,7 @@ from sqlalchemy import ForeignKey, String from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db if TYPE_CHECKING: from timApp.user.user import User @@ -109,7 +109,7 @@ def check_grant_type(self, grant_type: str) -> bool: return grant_type in self.grant_types -class OAuth2Token(DbModel, TokenMixin): +class OAuth2Token(db.Model, TokenMixin): __tablename__ = "oauth2_token" id: Mapped[int] = mapped_column(primary_key=True) @@ -146,7 +146,7 @@ def is_expired(self) -> bool: return expires_at < time.time() -class OAuth2AuthorizationCode(DbModel, AuthorizationCodeMixin): +class OAuth2AuthorizationCode(db.Model, AuthorizationCodeMixin): __tablename__ = "oauth2_auth_code" id: Mapped[int] = mapped_column(primary_key=True) diff --git a/timApp/auth/session/model.py b/timApp/auth/session/model.py index ec0f9a6e8a..818320db17 100755 --- a/timApp/auth/session/model.py +++ b/timApp/auth/session/model.py @@ -8,14 +8,14 @@ from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db from timApp.util.utils import get_current_time if TYPE_CHECKING: from timApp.user.user import User -class UserSession(DbModel): +class UserSession(db.Model): """ User session. A session is given to the user when they log in. diff --git a/timApp/document/docentry.py b/timApp/document/docentry.py index 3d372301c6..a80496fcec 100644 --- a/timApp/document/docentry.py +++ b/timApp/document/docentry.py @@ -13,7 +13,6 @@ from timApp.item.block import insert_block from timApp.timdb.exceptions import ItemAlreadyExistsException from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup, get_admin_group_id from timApp.util.utils import split_location @@ -21,7 +20,7 @@ from timApp.user.user import User -class DocEntry(DbModel, DocInfo): +class DocEntry(db.Model, DocInfo): """Represents a TIM document in the directory hierarchy. A document can have several aliases, which is why the primary key is "name" column and not "id". diff --git a/timApp/document/documents.py b/timApp/document/documents.py index bed35eadf6..0ed7b6350b 100644 --- a/timApp/document/documents.py +++ b/timApp/document/documents.py @@ -83,7 +83,6 @@ def add_reference_pars( doc: Document, original_doc: Document, r: str, translator: str | None = None ): for par in original_doc: - # If the paragraph is a citation, it should remain a citation in the translation # instead of being converted into a 'regular' translated paragraph. # Additionally, we want to check for a translated version of the citation that diff --git a/timApp/document/editing/routes.py b/timApp/document/editing/routes.py index 9de40d022d..e0e62899b1 100644 --- a/timApp/document/editing/routes.py +++ b/timApp/document/editing/routes.py @@ -56,6 +56,7 @@ from timApp.plugin.qst.qst import question_convert_js_to_yaml from timApp.plugin.save_plugin import save_plugin from timApp.readmark.readings import mark_read + # from timApp.timdb.dbaccess import get_timdb from timApp.timdb.exceptions import TimDbException from timApp.timdb.sqa import db, run_sql diff --git a/timApp/document/translation/language.py b/timApp/document/translation/language.py index c016201452..3473db749a 100644 --- a/timApp/document/translation/language.py +++ b/timApp/document/translation/language.py @@ -20,10 +20,9 @@ from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import DbModel -class Language(DbModel): +class Language(db.Model): """Represents a standardized language code used for example with translation documents. diff --git a/timApp/document/translation/reversingtranslator.py b/timApp/document/translation/reversingtranslator.py index c8a27f5e8d..d73aa1cf75 100644 --- a/timApp/document/translation/reversingtranslator.py +++ b/timApp/document/translation/reversingtranslator.py @@ -26,7 +26,6 @@ Usage, ) - REVERSE_LANG = { "lang_code": langcodes.standardize_tag("rev-erse"), "lang_name": "Reverse", @@ -115,7 +114,7 @@ def languages(self) -> LanguagePairing: lang = Language.query_by_code(REVERSE_LANG["lang_code"]) if lang is not None: return LanguagePairing( - value={source: lang for source in Language.query_all()} + value={source.lang_code: [lang] for source in Language.query_all()} ) raise Exception( f"Test-language is not found in database with the code '{REVERSE_LANG['lang_code']}'." diff --git a/timApp/document/translation/translation.py b/timApp/document/translation/translation.py index 8dada35d15..4792c7b297 100644 --- a/timApp/document/translation/translation.py +++ b/timApp/document/translation/translation.py @@ -5,14 +5,13 @@ from timApp.document.docinfo import DocInfo from timApp.timdb.sqa import db -from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.item.block import Block from timApp.document.docentry import DocEntry -class Translation(DbModel, DocInfo): +class Translation(db.Model, DocInfo): """A translated document. Translation objects may be created in two scenarios: diff --git a/timApp/document/translation/translator.py b/timApp/document/translation/translator.py index ff40bfbf12..8b03f964da 100644 --- a/timApp/document/translation/translator.py +++ b/timApp/document/translation/translator.py @@ -34,8 +34,7 @@ Table, Translate, ) -from timApp.timdb.sqa import run_sql -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import run_sql, db from timApp.user.usergroup import UserGroup from timApp.util import logger from timApp.util.flask.requesthelper import RouteException @@ -71,7 +70,7 @@ def __getitem__(self, item: str) -> list[Language]: return self.value[item] -class TranslationService(DbModel): +class TranslationService(db.Model): """Represents the information and methods that must be available from all possible machine translators. """ @@ -172,7 +171,7 @@ def get_languages(self, source_langs: bool) -> list[Language]: __mapper_args__ = {"polymorphic_on": "service_name"} -class TranslationServiceKey(DbModel): +class TranslationServiceKey(db.Model): """Represents an API-key (or any string value) that is needed for using a machine translator and that one or more users are in possession of. """ diff --git a/timApp/folder/folder.py b/timApp/folder/folder.py index 07dee248a4..45506358da 100644 --- a/timApp/folder/folder.py +++ b/timApp/folder/folder.py @@ -14,7 +14,6 @@ from timApp.item.item import Item from timApp.timdb.exceptions import ItemAlreadyExistsException from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup from timApp.util.utils import split_location, join_location, relative_location @@ -24,7 +23,7 @@ ROOT_FOLDER_ID = -1 -class Folder(DbModel, Item): +class Folder(db.Model, Item): """Represents a folder in the directory hierarchy.""" id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) diff --git a/timApp/item/block.py b/timApp/item/block.py index 06eeec05db..d03fbac741 100644 --- a/timApp/item/block.py +++ b/timApp/item/block.py @@ -16,7 +16,7 @@ from timApp.auth.auth_models import BlockAccess from timApp.item.blockassociation import BlockAssociation from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz from timApp.user.usergroup import UserGroup from timApp.user.usergroupdoc import UserGroupDoc from timApp.util.utils import get_current_time @@ -36,7 +36,7 @@ ) -class Block(DbModel): +class Block(db.Model): """The "base class" for all database objects that are part of the permission system.""" id: Mapped[int] = mapped_column(primary_key=True) diff --git a/timApp/item/blockassociation.py b/timApp/item/blockassociation.py index 580c3aad90..339b70b0f3 100644 --- a/timApp/item/blockassociation.py +++ b/timApp/item/blockassociation.py @@ -1,10 +1,10 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db -class BlockAssociation(DbModel): +class BlockAssociation(db.Model): """Associates blocks with other blocks. Currently only used for associating uploaded files with documents.""" parent: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) diff --git a/timApp/item/blockrelevance.py b/timApp/item/blockrelevance.py index 60124d650c..376dfb9061 100644 --- a/timApp/item/blockrelevance.py +++ b/timApp/item/blockrelevance.py @@ -3,13 +3,13 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db if TYPE_CHECKING: from timApp.item.block import Block -class BlockRelevance(DbModel): +class BlockRelevance(db.Model): """A relevance value of a block (used in search).""" block_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) diff --git a/timApp/item/tag.py b/timApp/item/tag.py index 96306d32fb..64e4e9199a 100644 --- a/timApp/item/tag.py +++ b/timApp/item/tag.py @@ -4,7 +4,8 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.item.block import Block @@ -24,7 +25,7 @@ class TagType(Enum): """The Tag is the name for a subject.""" -class Tag(DbModel): +class Tag(db.Model): """A tag with associated document id, tag name, type and expiration date.""" block_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) diff --git a/timApp/item/taskblock.py b/timApp/item/taskblock.py index bd02c7657f..01979a73a3 100644 --- a/timApp/item/taskblock.py +++ b/timApp/item/taskblock.py @@ -5,11 +5,10 @@ from timApp.item.block import Block, BlockType, insert_block from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import DbModel from timApp.user.usergroup import UserGroup -class TaskBlock(DbModel): +class TaskBlock(db.Model): id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) task_id: Mapped[str] = mapped_column(primary_key=True) diff --git a/timApp/lecture/askedjson.py b/timApp/lecture/askedjson.py index efd68e8f9f..c8d6097229 100644 --- a/timApp/lecture/askedjson.py +++ b/timApp/lecture/askedjson.py @@ -5,14 +5,13 @@ from sqlalchemy import select from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import run_sql -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import run_sql, db if TYPE_CHECKING: from timApp.lecture.askedquestion import AskedQuestion -class AskedJson(DbModel): +class AskedJson(db.Model): asked_json_id: Mapped[int] = mapped_column(primary_key=True) json: Mapped[str] hash: Mapped[str] diff --git a/timApp/lecture/askedquestion.py b/timApp/lecture/askedquestion.py index f295aa2a32..a8fb7ff89e 100644 --- a/timApp/lecture/askedquestion.py +++ b/timApp/lecture/askedquestion.py @@ -9,7 +9,7 @@ from timApp.lecture.question_utils import qst_rand_array, qst_filter_markup_points from timApp.lecture.questionactivity import QuestionActivityKind, QuestionActivity from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz from timApp.util.utils import get_current_time if TYPE_CHECKING: @@ -21,7 +21,7 @@ from timApp.lecture.showpoints import ShowPoints -class AskedQuestion(DbModel): +class AskedQuestion(db.Model): asked_id: Mapped[int] = mapped_column(primary_key=True) lecture_id: Mapped[int] = mapped_column(ForeignKey("lecture.lecture_id")) doc_id: Mapped[Optional[int]] = mapped_column(ForeignKey("block.id")) diff --git a/timApp/lecture/lecture.py b/timApp/lecture/lecture.py index f08c8b13b0..c6188bfe95 100644 --- a/timApp/lecture/lecture.py +++ b/timApp/lecture/lecture.py @@ -7,7 +7,7 @@ from timApp.lecture.lectureusers import LectureUsers from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz from timApp.util.utils import get_current_time if TYPE_CHECKING: @@ -18,7 +18,7 @@ from timApp.lecture.useractivity import UserActivity -class Lecture(DbModel): +class Lecture(db.Model): lecture_id: Mapped[int] = mapped_column(primary_key=True) lecture_code: Mapped[Optional[str]] doc_id: Mapped[int] = mapped_column(ForeignKey("block.id")) diff --git a/timApp/lecture/lectureanswer.py b/timApp/lecture/lectureanswer.py index 5a3e12fd5c..ac83abe936 100644 --- a/timApp/lecture/lectureanswer.py +++ b/timApp/lecture/lectureanswer.py @@ -7,7 +7,7 @@ from timApp.lecture.lecture import Lecture from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz from timApp.user.user import User if TYPE_CHECKING: @@ -28,7 +28,7 @@ def unshuffle_lectureanswer( return unshuffled_ans -class LectureAnswer(DbModel): +class LectureAnswer(db.Model): answer_id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) question_id: Mapped[int] = mapped_column(ForeignKey("askedquestion.asked_id")) diff --git a/timApp/lecture/lectureusers.py b/timApp/lecture/lectureusers.py index a4926510aa..cfe39e9135 100644 --- a/timApp/lecture/lectureusers.py +++ b/timApp/lecture/lectureusers.py @@ -1,10 +1,10 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db -class LectureUsers(DbModel): +class LectureUsers(db.Model): lecture_id: Mapped[int] = mapped_column( ForeignKey("lecture.lecture_id"), primary_key=True ) diff --git a/timApp/lecture/message.py b/timApp/lecture/message.py index bac7b5525b..a3c9117593 100644 --- a/timApp/lecture/message.py +++ b/timApp/lecture/message.py @@ -4,14 +4,15 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.lecture.lecture import Lecture from timApp.user.user import User -class Message(DbModel): +class Message(db.Model): msg_id: Mapped[int] = mapped_column(primary_key=True) lecture_id: Mapped[int] = mapped_column(ForeignKey("lecture.lecture_id")) user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) diff --git a/timApp/lecture/question.py b/timApp/lecture/question.py index 0a03739156..812bba8b20 100644 --- a/timApp/lecture/question.py +++ b/timApp/lecture/question.py @@ -3,10 +3,10 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db -class Question(DbModel): +class Question(db.Model): question_id: Mapped[int] = mapped_column(primary_key=True) doc_id: Mapped[int] = mapped_column(ForeignKey("block.id")) par_id: Mapped[str] diff --git a/timApp/lecture/questionactivity.py b/timApp/lecture/questionactivity.py index c50adb0d36..4f185e5c08 100644 --- a/timApp/lecture/questionactivity.py +++ b/timApp/lecture/questionactivity.py @@ -4,7 +4,7 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db if TYPE_CHECKING: from timApp.lecture.askedquestion import AskedQuestion @@ -19,7 +19,7 @@ class QuestionActivityKind(Enum): Usershown = 5 -class QuestionActivity(DbModel): +class QuestionActivity(db.Model): __tablename__ = "question_activity" asked_id: Mapped[int] = mapped_column( diff --git a/timApp/lecture/runningquestion.py b/timApp/lecture/runningquestion.py index 1569d22e35..8d01eb680e 100644 --- a/timApp/lecture/runningquestion.py +++ b/timApp/lecture/runningquestion.py @@ -4,14 +4,15 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.lecture.askedquestion import AskedQuestion from timApp.lecture.lecture import Lecture -class RunningQuestion(DbModel): +class RunningQuestion(db.Model): asked_id: Mapped[int] = mapped_column( ForeignKey("askedquestion.asked_id"), primary_key=True ) diff --git a/timApp/lecture/showpoints.py b/timApp/lecture/showpoints.py index b8d8771f18..1f8dcc67a9 100644 --- a/timApp/lecture/showpoints.py +++ b/timApp/lecture/showpoints.py @@ -3,13 +3,13 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db if TYPE_CHECKING: from timApp.lecture.askedquestion import AskedQuestion -class ShowPoints(DbModel): +class ShowPoints(db.Model): asked_id: Mapped[int] = mapped_column( ForeignKey("askedquestion.asked_id"), primary_key=True ) diff --git a/timApp/lecture/useractivity.py b/timApp/lecture/useractivity.py index 4faa75014f..98278aea8d 100644 --- a/timApp/lecture/useractivity.py +++ b/timApp/lecture/useractivity.py @@ -4,14 +4,14 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.user.user import User from timApp.lecture.lecture import Lecture -class UserActivity(DbModel): +class UserActivity(db.Model): lecture_id: Mapped[int] = mapped_column( ForeignKey("lecture.lecture_id"), primary_key=True ) diff --git a/timApp/messaging/messagelist/messagelist_models.py b/timApp/messaging/messagelist/messagelist_models.py index 19f04bdbf1..40910328f1 100644 --- a/timApp/messaging/messagelist/messagelist_models.py +++ b/timApp/messaging/messagelist/messagelist_models.py @@ -15,8 +15,8 @@ Distribution, MessageVerificationType, ) -from timApp.timdb.sqa import run_sql -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import run_sql, db +from timApp.timdb.types import datetime_tz from timApp.user.usergroup import UserGroup from timApp.util.utils import get_current_time @@ -36,7 +36,7 @@ class MemberJoinMethod(Enum): """User joined the list on their own.""" -class MessageListModel(DbModel): +class MessageListModel(db.Model): """Database model for message lists""" __tablename__ = "messagelist" @@ -225,7 +225,11 @@ def find_member( raise ValueError for member in self.members: - if username and username == member.get_username(): + if ( + username + and isinstance(member, MessageListTimMember) + and username == member.get_username() + ): return member if email and email == member.get_email(): return member @@ -266,7 +270,7 @@ def to_info(self) -> ListInfo: ) -class MessageListMember(DbModel): +class MessageListMember(db.Model): """Database model for members of a message list.""" __tablename__ = "messagelist_member" @@ -393,8 +397,8 @@ def to_json(self) -> dict[str, Any]: return { **user_info, - "sendRight": self.member.send_right, - "deliveryRight": self.member.delivery_right, + "sendRight": self.send_right, + "deliveryRight": self.delivery_right, "removed": self.membership_ended, } @@ -500,7 +504,7 @@ def get_username(self) -> str: return "" -class MessageListDistribution(DbModel): +class MessageListDistribution(db.Model): """Message list member's chosen distribution channels.""" __tablename__ = "messagelist_distribution" diff --git a/timApp/messaging/messagelist/routes.py b/timApp/messaging/messagelist/routes.py index 5b9273acfb..12d321c437 100644 --- a/timApp/messaging/messagelist/routes.py +++ b/timApp/messaging/messagelist/routes.py @@ -439,7 +439,9 @@ def get_group_members(list_name: str) -> Response: # Get group. groups: list[MessageListTimMember] = [ - member for member in message_list.members if member.is_group() + member + for member in message_list.members + if isinstance(member, MessageListTimMember) and member.is_group() ] # At this point we assume we have a user that is a TIM user group. diff --git a/timApp/messaging/timMessage/internalmessage_models.py b/timApp/messaging/timMessage/internalmessage_models.py index 6ecfe74f76..b56e0f3714 100644 --- a/timApp/messaging/timMessage/internalmessage_models.py +++ b/timApp/messaging/timMessage/internalmessage_models.py @@ -5,8 +5,8 @@ from sqlalchemy import func, select, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import run_sql -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import run_sql, db +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.user.user import User @@ -19,7 +19,7 @@ class DisplayType(Enum): STICKY = 2 -class InternalMessage(DbModel): +class InternalMessage(db.Model): """A TIM message.""" __tablename__ = "internalmessage" @@ -73,7 +73,7 @@ def to_json(self) -> dict[str, Any]: } -class InternalMessageDisplay(DbModel): +class InternalMessageDisplay(db.Model): """Where and for whom a TIM message is displayed.""" __tablename__ = "internalmessage_display" @@ -110,7 +110,7 @@ def to_json(self) -> dict[str, Any]: } -class InternalMessageReadReceipt(DbModel): +class InternalMessageReadReceipt(db.Model): """Metadata about read receipts.""" __tablename__ = "internalmessage_readreceipt" diff --git a/timApp/messaging/timMessage/routes.py b/timApp/messaging/timMessage/routes.py index dd6ebaa093..e09f348c4f 100644 --- a/timApp/messaging/timMessage/routes.py +++ b/timApp/messaging/timMessage/routes.py @@ -181,7 +181,7 @@ def get_tim_messages_as_list(item_id: int | None = None) -> list[TimMessageData] ) if item_id is not None: - current_page_obj = DocEntry.find_by_id(item_id) + current_page_obj: DocInfo | Folder | None = DocEntry.find_by_id(item_id) if isinstance(current_page_obj, Translation): # Resolve to original file instead of translation file current_page_obj = current_page_obj.docentry @@ -322,7 +322,9 @@ def check_urls(urls: str) -> Response: shortened_url = URL_PATTERN.sub("", url) else: shortened_url = url - document = DocEntry.find_by_path(shortened_url) # check if url exists in TIM + document: DocInfo | Folder | None = DocEntry.find_by_path( + shortened_url + ) # check if url exists in TIM if document is None: document = Folder.find_by_path(shortened_url) if document is None: diff --git a/timApp/note/usernote.py b/timApp/note/usernote.py index 1d17e310f2..6dbe5f9d8f 100644 --- a/timApp/note/usernote.py +++ b/timApp/note/usernote.py @@ -4,14 +4,14 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.user.usergroup import UserGroup from timApp.item.block import Block -class UserNote(DbModel): +class UserNote(db.Model): """A comment/note that has been posted in a document paragraph.""" __tablename__ = "usernotes" diff --git a/timApp/notification/notification.py b/timApp/notification/notification.py index 5a8864197b..33d2c06707 100644 --- a/timApp/notification/notification.py +++ b/timApp/notification/notification.py @@ -5,8 +5,7 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.item.block import BlockType, Block -from timApp.timdb.sqa import is_attribute_loaded -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import is_attribute_loaded, db from timApp.util.logger import log_warning if TYPE_CHECKING: @@ -34,7 +33,7 @@ def is_document_modification(self) -> bool: ) -class Notification(DbModel): +class Notification(db.Model): """Notification settings for a User for a block.""" user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) diff --git a/timApp/notification/pending_notification.py b/timApp/notification/pending_notification.py index 9eb532a7eb..3dc1b22d6c 100644 --- a/timApp/notification/pending_notification.py +++ b/timApp/notification/pending_notification.py @@ -5,8 +5,8 @@ from timApp.document.version import Version from timApp.notification.notification import NotificationType -from timApp.timdb.sqa import run_sql -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import run_sql, db +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.user.user import User @@ -15,7 +15,7 @@ GroupingKey = tuple[int, str] -class PendingNotification(DbModel): +class PendingNotification(db.Model): id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) doc_id: Mapped[int] = mapped_column(ForeignKey("block.id")) diff --git a/timApp/peerreview/peerreview.py b/timApp/peerreview/peerreview.py index f986487eb4..3498ec81a3 100644 --- a/timApp/peerreview/peerreview.py +++ b/timApp/peerreview/peerreview.py @@ -4,13 +4,13 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.user.user import User -class PeerReview(DbModel): +class PeerReview(db.Model): """A peer review to a task.""" __tablename__ = "peer_review" diff --git a/timApp/peerreview/util/groups.py b/timApp/peerreview/util/groups.py index 68c59819c7..b0dc58c880 100644 --- a/timApp/peerreview/util/groups.py +++ b/timApp/peerreview/util/groups.py @@ -62,7 +62,7 @@ def generate_review_groups(doc: DocInfo, task_ids: list[TaskId]) -> None: f"(set to {review_count} but {len(users)} users have answered so far)" ) - for idx, user in enumerate(users): + for idx, _ in enumerate(users): pairings_left = review_count + 1 start = idx + 1 end = idx + pairings_left diff --git a/timApp/plugin/calendar/models.py b/timApp/plugin/calendar/models.py index d665c419db..453e7cbd11 100644 --- a/timApp/plugin/calendar/models.py +++ b/timApp/plugin/calendar/models.py @@ -19,7 +19,7 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz from timApp.user.user import User from timApp.user.usergroup import UserGroup from tim_common.dumboclient import call_dumbo @@ -28,7 +28,7 @@ from timApp.item.block import Block -class EventGroup(DbModel): +class EventGroup(db.Model): """Information about a user group participating in an event.""" event_id: Mapped[int] = mapped_column( @@ -51,7 +51,7 @@ class EventGroup(DbModel): """The usergroup that belongs to the group""" -class Enrollment(DbModel): +class Enrollment(db.Model): """A single enrollment in an event""" event_id: Mapped[int] = mapped_column( @@ -98,7 +98,7 @@ def get_by_event_and_user( ) -class EventTagAttachment(DbModel): +class EventTagAttachment(db.Model): """Attachment information for the event tag""" event_id: Mapped[int] = mapped_column( @@ -109,7 +109,7 @@ class EventTagAttachment(DbModel): """Tag that is attached to the event""" -class EventTag(DbModel): +class EventTag(db.Model): """A string tag that can be attached to an event""" tag_id: Mapped[int] = mapped_column(primary_key=True) @@ -171,7 +171,7 @@ def is_valid(self) -> bool: return self.can_enroll or self.can_manage_event -class Event(DbModel): +class Event(db.Model): """A calendar event. Event has metadata (title, time, location) and various participating user groups.""" event_id: Mapped[int] = mapped_column(primary_key=True) @@ -399,7 +399,7 @@ def to_json( } -class EnrollmentType(DbModel): +class EnrollmentType(db.Model): """Table for enrollment type, combines enrollment type ID to specific enrollment type""" enroll_type_id: Mapped[int] = mapped_column(primary_key=True) @@ -409,7 +409,7 @@ class EnrollmentType(DbModel): """Name of the enrollment type""" -class ExportedCalendar(DbModel): +class ExportedCalendar(db.Model): """Information about exported calendars""" user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) diff --git a/timApp/plugin/jsrunner/util.py b/timApp/plugin/jsrunner/util.py index 3756e36ba3..940bf93c6c 100644 --- a/timApp/plugin/jsrunner/util.py +++ b/timApp/plugin/jsrunner/util.py @@ -30,7 +30,7 @@ ) from timApp.peerreview.util.peerreview_utils import change_peerreviewers_for_user from timApp.plugin.importdata.importData import MissingUser, MissingUserSchema -from timApp.plugin.plugin import TaskNotFoundException, CachedPluginFinder +from timApp.plugin.plugin import TaskNotFoundException, CachedPluginFinder, Plugin from timApp.plugin.pluginexception import PluginException from timApp.plugin.plugintype import PluginType from timApp.plugin.taskid import TaskId, TaskIdAccess @@ -302,7 +302,7 @@ def save_fields( UserContext.from_one_user(curr_user), view_ctx, ) - plugin = vr.plugin + plugin: PluginType | Plugin = vr.plugin except TaskNotFoundException as e: if not allow_missing: if ignore_missing: @@ -604,7 +604,7 @@ def _handle_item_right_actions( # Apply actions as we go for item_id, actions in item_actions.items(): - item = items[item_id] + itm = items[item_id] for action in actions: group = UserGroup.get_by_name(action.group) access_type = action.accessType or AccessType.view @@ -614,13 +614,13 @@ def _handle_item_right_actions( case "add": grant_access( group, - item, + itm, access_type, accessible_from=action.accessibleFrom, accessible_to=action.accessibleTo, ) case "expire": - expire_access(group, item, access_type) + expire_access(group, itm, access_type) # Flush so that access rights are correct for any other JSRunner operations db.session.flush() diff --git a/timApp/plugin/plugintype.py b/timApp/plugin/plugintype.py index f05fa53ea4..f41a2bbdd6 100644 --- a/timApp/plugin/plugintype.py +++ b/timApp/plugin/plugintype.py @@ -8,7 +8,6 @@ import timApp from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import DbModel CONTENT_FIELD_NAME_MAP = { "csPlugin": "usercode", @@ -35,7 +34,7 @@ def to_json(self) -> dict[str, Any]: # TODO: Right now values are added dynamically to the table when saving answers. Instead add them on TIM start. -class PluginType(DbModel, PluginTypeBase): +class PluginType(db.Model, PluginTypeBase): id: Mapped[int] = mapped_column(primary_key=True) type: Mapped[str] = mapped_column(unique=True) diff --git a/timApp/plugin/quantum_circuit/__init__.py b/timApp/plugin/quantum_circuit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/timApp/plugin/quantum_circuit/quantumCircuit.py b/timApp/plugin/quantum_circuit/quantumCircuit.py index c1dbfbfb91..ee489b08c0 100644 --- a/timApp/plugin/quantum_circuit/quantumCircuit.py +++ b/timApp/plugin/quantum_circuit/quantumCircuit.py @@ -1,21 +1,21 @@ -from dataclasses import dataclass, asdict -from typing import Union import json +import math import re from collections import defaultdict -import math +from dataclasses import dataclass, asdict from threading import Thread +from typing import Union -from flask import render_template_string, request, jsonify, Response +import numpy as np import yaml - +from flask import render_template_string, request, jsonify, Response from qulacs import QuantumCircuit, QuantumState, QuantumGateMatrix from qulacs.gate import H, X, Y, Z, S, T, to_matrix_gate, DenseMatrix -import numpy as np from timApp.auth.accesshelper import verify_logged_in from timApp.tim_app import csrf from timApp.util.flask.requesthelper import use_model +from timApp.util.logger import log_warning from tim_common.markupmodels import GenericMarkupModel from tim_common.pluginserver_flask import ( GenericHtmlModel, @@ -26,7 +26,6 @@ EditorTab, ) from tim_common.utils import Missing -from timApp.util.logger import log_warning @dataclass @@ -528,7 +527,6 @@ def run_all_simulations_threaded( model_input: list[str] | None, max_run_timeout: int | None, ) -> tuple[bool, ErrorType | None]: - sim_params = ThreadedSimParams(False, (True, None)) # allow at max 25 seconds of simulation time @@ -672,7 +670,7 @@ def answer(args: QuantumCircuitAnswerModel) -> PluginAnswerResp: ) if not is_valid: valid_conditions = False - error = asdict(message) + error = asdict(message) if message else "Unknown error" points = 0.0 result = "" if ( @@ -696,7 +694,7 @@ def answer(args: QuantumCircuitAnswerModel) -> PluginAnswerResp: else: points = 0.0 result = "" - error = asdict(sim_error) + error = asdict(sim_error) if sim_error else "Unknown error" return { "save": {"userCircuit": user_circuit, "userInput": user_input}, @@ -836,12 +834,14 @@ def quantum_circuit_simulate(args: SimulationArgs) -> Response: ) if success and isinstance(result, np.ndarray): return jsonify({"web": {"result": list(result), "error": ""}}) - + error_message = ( + asdict(result) if not isinstance(result, np.ndarray) else "Unknown result" + ) return jsonify( { "web": { "result": [], - "error": json.dumps(asdict(result), ensure_ascii=False, indent=4), + "error": json.dumps(error_message, ensure_ascii=False, indent=4), } } ) diff --git a/timApp/plugin/tableform/tableForm.py b/timApp/plugin/tableform/tableForm.py index e19ca1be16..21c19fcc2a 100644 --- a/timApp/plugin/tableform/tableForm.py +++ b/timApp/plugin/tableform/tableForm.py @@ -792,7 +792,7 @@ def tableform_get_fields( membership_end_map: dict[str, str | None] = {} for f in fielddata: u: User = f["user"] - u.hide_name = anonymize_names + u.is_name_hidden = anonymize_names user_info = u.to_json() username = user_info["name"] rn = user_info["real_name"] diff --git a/timApp/plugin/timtable/row_owner_info.py b/timApp/plugin/timtable/row_owner_info.py index 6233d79777..ceabfd91ba 100644 --- a/timApp/plugin/timtable/row_owner_info.py +++ b/timApp/plugin/timtable/row_owner_info.py @@ -3,10 +3,10 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db -class RowOwnerInfo(DbModel): +class RowOwnerInfo(db.Model): """ Information about the owner of a TimTable row. Includes document and paragraph id for determining the TimTable instance. diff --git a/timApp/printing/printeddoc.py b/timApp/printing/printeddoc.py index 7148cd1f68..9f16abc954 100644 --- a/timApp/printing/printeddoc.py +++ b/timApp/printing/printeddoc.py @@ -4,10 +4,10 @@ from sqlalchemy.orm import mapped_column, Mapped from timApp.timdb.sqa import db -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz -class PrintedDoc(DbModel): +class PrintedDoc(db.Model): """A printed document. A PrintedDoc is created each time a document is printed (CSS printing does not count because it happens entirely in browser).""" diff --git a/timApp/readmark/readparagraph.py b/timApp/readmark/readparagraph.py index bdd3966480..b1bf680ab9 100644 --- a/timApp/readmark/readparagraph.py +++ b/timApp/readmark/readparagraph.py @@ -4,13 +4,14 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.readmark.readparagraphtype import ReadParagraphType -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.user.usergroup import UserGroup -class ReadParagraph(DbModel): +class ReadParagraph(db.Model): """Denotes that a User(Group) has read a specific paragraph in some way.""" id: Mapped[int] = mapped_column(primary_key=True) diff --git a/timApp/sisu/scim.py b/timApp/sisu/scim.py index e45c888bcf..38e8e1896a 100644 --- a/timApp/sisu/scim.py +++ b/timApp/sisu/scim.py @@ -518,10 +518,10 @@ def update_users(ug: UserGroup, args: SCIMGroupModel) -> None: ug.external_id.is_responsible_teacher and not ug.external_id.is_studysubgroup ) or ug.external_id.is_administrative_person: tg = UserGroup.get_teachers_group() - for u in added_users: - if tg not in u.groups: - u.groups.append(tg) - send_course_group_mail(p, u) + for usr in added_users: + if tg not in usr.groups: + usr.groups.append(tg) + send_course_group_mail(p, usr) def parse_sisu_group_display_name_or_error(args: SCIMGroupModel) -> SisuDisplayName: diff --git a/timApp/sisu/scimusergroup.py b/timApp/sisu/scimusergroup.py index 20ad7ee77f..dbf3834661 100644 --- a/timApp/sisu/scimusergroup.py +++ b/timApp/sisu/scimusergroup.py @@ -3,7 +3,7 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db uuid_re = "[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}" external_id_re = re.compile( @@ -11,7 +11,7 @@ ) -class ScimUserGroup(DbModel): +class ScimUserGroup(db.Model): group_id: Mapped[int] = mapped_column(ForeignKey("usergroup.id"), primary_key=True) external_id: Mapped[str] = mapped_column(unique=True) diff --git a/timApp/slide/slidestatus.py b/timApp/slide/slidestatus.py index 7ab43a08c9..bb25968d84 100644 --- a/timApp/slide/slidestatus.py +++ b/timApp/slide/slidestatus.py @@ -1,10 +1,10 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db -class SlideStatus(DbModel): +class SlideStatus(db.Model): __tablename__ = "slide_status" doc_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) diff --git a/timApp/timdb/sqa.py b/timApp/timdb/sqa.py index 04af4b02ae..f99d8e1ed1 100644 --- a/timApp/timdb/sqa.py +++ b/timApp/timdb/sqa.py @@ -23,12 +23,14 @@ # because sometimes objects would expire after calling a route. session_options["expire_on_commit"] = False -db = SQLAlchemy(session_options=session_options, model_class=DbModel) +db = SQLAlchemy( + session_options=session_options, model_class=DbModel, disable_autonaming=True +) # Overwrite metadata to use the DbModel's metadata # Flask-SQLAlchemy 3.x doesn't appear to have a correct handler of model_class, so it ends up overwriting our DbModel # Instead, we pass our model manually -db.Model = DbModel -db.metadatas[None] = DbModel.metadata +# db.Model = DbModel +# db.metadatas[None] = DbModel.metadata # TODO: Switch models to use dataclasses instead diff --git a/timApp/timdb/types.py b/timApp/timdb/types.py index 1528831093..1f23715d38 100644 --- a/timApp/timdb/types.py +++ b/timApp/timdb/types.py @@ -5,20 +5,23 @@ from flask_sqlalchemy.model import Model # type: ignore from sqlalchemy import Text, DateTime from sqlalchemy.orm import DeclarativeBase, declared_attr, has_inherited_table +from sqlalchemy.orm import registry from typing_extensions import Annotated datetime_tz = Annotated[datetime, "datetime_tz"] -class DbModel(DeclarativeBase, Model): +class DbModel(DeclarativeBase): """ Base class for all TIM database models. """ - type_annotation_map = { - str: Text, - datetime_tz: DateTime(timezone=True), - } + registry = registry( + type_annotation_map={ + str: Text, + datetime_tz: DateTime(timezone=True), + } + ) # Add check for mypy to suppress __tablename__ error when it's overridden as a string and not a method if not TYPE_CHECKING: diff --git a/timApp/user/consentchange.py b/timApp/user/consentchange.py index a88d9f8da3..e1e64256b7 100644 --- a/timApp/user/consentchange.py +++ b/timApp/user/consentchange.py @@ -1,11 +1,12 @@ from sqlalchemy import func, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.user.user import Consent, User -class ConsentChange(DbModel): +class ConsentChange(db.Model): id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id")) time: Mapped[datetime_tz] = mapped_column(default=func.now()) diff --git a/timApp/user/hakaorganization.py b/timApp/user/hakaorganization.py index 04f7c6795b..2a29145528 100644 --- a/timApp/user/hakaorganization.py +++ b/timApp/user/hakaorganization.py @@ -6,13 +6,12 @@ from sqlalchemy.orm import mapped_column, Mapped, relationship from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.user.personaluniquecode import PersonalUniqueCode -class HakaOrganization(DbModel): +class HakaOrganization(db.Model): __tablename__ = "haka_organization" id: Mapped[int] = mapped_column(primary_key=True) diff --git a/timApp/user/newuser.py b/timApp/user/newuser.py index 344cf77084..7e44e15208 100644 --- a/timApp/user/newuser.py +++ b/timApp/user/newuser.py @@ -1,11 +1,12 @@ from sqlalchemy import func from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.user.userutils import check_password_hash -class NewUser(DbModel): +class NewUser(db.Model): """A user that is going to register to TIM via email and has not yet completed the registration process.""" email: Mapped[str] = mapped_column(primary_key=True) diff --git a/timApp/user/personaluniquecode.py b/timApp/user/personaluniquecode.py index 1e75dbe8d5..055a1dc448 100644 --- a/timApp/user/personaluniquecode.py +++ b/timApp/user/personaluniquecode.py @@ -5,15 +5,14 @@ from sqlalchemy import select, UniqueConstraint, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.sqa import run_sql -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import run_sql, db from timApp.user.hakaorganization import HakaOrganization if TYPE_CHECKING: from timApp.user.user import User -class PersonalUniqueCode(DbModel): +class PersonalUniqueCode(db.Model): """The database model for the 'schacPersonalUniqueCode' Haka attribute.""" __tablename__ = "personal_unique_code" diff --git a/timApp/user/settings/settings.py b/timApp/user/settings/settings.py index c6ac8ddff5..745a8abc06 100644 --- a/timApp/user/settings/settings.py +++ b/timApp/user/settings/settings.py @@ -161,7 +161,7 @@ def get_user_info(u: User, include_doc_content: bool = False) -> dict[str, Any]: for ann in annotations: for c in ann.comments: if c.commenter.id != u.id: - c.commenter.anonymize = True + c.commenter.is_anonymized = True return { "annotations": annotations, diff --git a/timApp/user/user.py b/timApp/user/user.py index ba90a88ead..69b212e9ef 100755 --- a/timApp/user/user.py +++ b/timApp/user/user.py @@ -41,7 +41,6 @@ from timApp.sisu.scimusergroup import ScimUserGroup from timApp.timdb.exceptions import TimDbException from timApp.timdb.sqa import db, TimeStampMixin, is_attribute_loaded, run_sql -from timApp.timdb.types import DbModel from timApp.user.hakaorganization import HakaOrganization, get_home_organization_id from timApp.user.personaluniquecode import SchacPersonalUniqueCode, PersonalUniqueCode from timApp.user.preferences import Preferences @@ -258,7 +257,7 @@ def user_query_with_joined_groups() -> Select: return select(User).options(selectinload(User.groups)) -class User(DbModel, TimeStampMixin, SCIMEntity): +class User(db.Model, TimeStampMixin, SCIMEntity): """A user account. Used to identify users. .. note:: Some user IDs are reserved for internal use: @@ -1176,6 +1175,10 @@ def skip_access_lock(self): """If set, access any access locking is skipped when checking for permissions.""" return getattr(self, "bypass_access_lock", False) + @skip_access_lock.setter + def skip_access_lock(self, value): + setattr(self, "bypass_access_lock", value) + def _downgrade_access( self, access_vals: set[int], access: BlockAccess | None ) -> BlockAccess | None: @@ -1467,11 +1470,19 @@ def is_name_hidden(self): """Hides names and email of the user, but not user ID""" return getattr(self, "hide_name", False) + @is_name_hidden.setter + def is_name_hidden(self, value): + setattr(self, "hide_name", value) + @property def is_anonymized(self): """Hides names, email and ID of the user""" return getattr(self, "anonymize", False) + @is_anonymized.setter + def is_anonymized(self, value): + setattr(self, "anonymize", value) + @property def is_anonymous_guest_user(self): return self.id < 0 diff --git a/timApp/user/usercontact.py b/timApp/user/usercontact.py index 078a3215c6..8140328d9a 100644 --- a/timApp/user/usercontact.py +++ b/timApp/user/usercontact.py @@ -6,7 +6,6 @@ from timApp.messaging.messagelist.listinfo import Channel from timApp.timdb.sqa import db -from timApp.timdb.types import DbModel if TYPE_CHECKING: from timApp.user.user import User @@ -37,7 +36,7 @@ class PrimaryContact(Enum): true = True -class UserContact(DbModel): +class UserContact(db.Model): """TIM users' additional contact information.""" __table_args__ = ( diff --git a/timApp/user/usergroup.py b/timApp/user/usergroup.py index 622149f94f..7989be22d9 100644 --- a/timApp/user/usergroup.py +++ b/timApp/user/usergroup.py @@ -24,7 +24,6 @@ is_attribute_loaded, run_sql, ) -from timApp.timdb.types import DbModel from timApp.user.scimentity import SCIMEntity from timApp.user.special_group_names import ( ANONYMOUS_GROUPNAME, @@ -64,7 +63,7 @@ def tim_group_to_scim(tim_group: str) -> str: ORG_GROUP_SUFFIX = " users" -class UserGroup(DbModel, TimeStampMixin, SCIMEntity): +class UserGroup(db.Model, TimeStampMixin, SCIMEntity): """A usergroup. Each User should belong to a personal UserGroup that has the same name as the User name. No one else should belong to a personal UserGroup. diff --git a/timApp/user/usergroupdoc.py b/timApp/user/usergroupdoc.py index 6e3fc62d6e..d31b79829a 100644 --- a/timApp/user/usergroupdoc.py +++ b/timApp/user/usergroupdoc.py @@ -1,10 +1,10 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped -from timApp.timdb.types import DbModel +from timApp.timdb.sqa import db -class UserGroupDoc(DbModel): +class UserGroupDoc(db.Model): """Each UserGroup can have at most one administrative document. The rights of that document determine who can see and edit the members of the UserGroup. """ diff --git a/timApp/user/usergroupmember.py b/timApp/user/usergroupmember.py index c262acd3da..9cb4edfd86 100644 --- a/timApp/user/usergroupmember.py +++ b/timApp/user/usergroupmember.py @@ -16,7 +16,8 @@ from sqlalchemy import func, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz from timApp.util.utils import get_current_time if TYPE_CHECKING: @@ -24,7 +25,7 @@ from timApp.user.usergroup import UserGroup -class UserGroupMember(DbModel): +class UserGroupMember(db.Model): """ Associates a user with a user group. """ diff --git a/timApp/user/verification/verification.py b/timApp/user/verification/verification.py index 3ec61f6497..374bbe5c3a 100644 --- a/timApp/user/verification/verification.py +++ b/timApp/user/verification/verification.py @@ -9,7 +9,7 @@ from timApp.document.docentry import DocEntry from timApp.timdb.sqa import db, run_sql -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.types import datetime_tz from timApp.user.user import User from timApp.user.usercontact import UserContact, PrimaryContact from timApp.util.utils import get_current_time @@ -46,7 +46,7 @@ def parse(t: str) -> Optional["VerificationType"]: } -class Verification(DbModel): +class Verification(db.Model): """For various pending verifications, such as message list joining and contact information ownership verification.""" @@ -113,7 +113,7 @@ def to_json(self) -> dict: "contact": self.contact.contact, } - __mapper_args__ = {"polymorphic_identity": VerificationType.CONTACT_OWNERSHIP} + __mapper_args__ = {"polymorphic_identity": VerificationType.CONTACT_OWNERSHIP} # type: ignore class SetPrimaryContactVerification(ContactAddVerification): @@ -159,7 +159,7 @@ def approve(self) -> None: u._email = self.contact.contact update_mailing_list_address(old_email, self.contact.contact) - __mapper_args__ = {"polymorphic_identity": VerificationType.SET_PRIMARY_CONTACT} + __mapper_args__ = {"polymorphic_identity": VerificationType.SET_PRIMARY_CONTACT} # type: ignore def send_verification_impl( diff --git a/timApp/util/error_handlers.py b/timApp/util/error_handlers.py index 82ff3d9717..7d806c8163 100644 --- a/timApp/util/error_handlers.py +++ b/timApp/util/error_handlers.py @@ -30,6 +30,7 @@ from timApp.auth.session.util import SessionExpired from timApp.auth.sessioninfo import get_current_user_object, clear_session from timApp.document.docentry import DocEntry +from timApp.document.docinfo import DocInfo from timApp.document.docsettings import get_minimal_visibility_settings from timApp.folder.folder import Folder from timApp.notification.send_email import send_email @@ -267,21 +268,19 @@ def handle_dumbo_html_exception(error: DumboHTMLException) -> ResponseReturnValu @app.errorhandler(ItemLockedException) def handle_item_locked(error: ItemLockedException) -> ResponseReturnValue: - item = DocEntry.find_by_id(error.access.block_id) - is_folder = False + item: DocInfo | Folder | None = DocEntry.find_by_id(error.access.block_id) if not item: - is_folder = True item = Folder.get_by_id(error.access.block_id) if not item: raise NotExist() view_settings = get_minimal_visibility_settings( - item.document if not is_folder else None + item.document if not isinstance(item, Folder) else None ) return ( render_template( "duration_unlock.jinja2", item=item, - item_type="folder" if is_folder else "document", + item_type="folder" if isinstance(item, Folder) else "document", access=error.access, msg=error.msg, next_doc=error.next_doc, diff --git a/timApp/velp/annotation.py b/timApp/velp/annotation.py index 0dbf225a9a..679760df28 100644 --- a/timApp/velp/annotation.py +++ b/timApp/velp/annotation.py @@ -286,10 +286,10 @@ def anonymize_annotations(anns: list[Annotation], current_user_id: int) -> None: """ for ann in anns: if ann.annotator.id != current_user_id: - ann.annotator.anonymize = True + ann.annotator.is_anonymized = True for c in ann.comments: if c.commenter.id != current_user_id: - c.commenter.anonymize = True + c.commenter.is_anonymized = True @annotations.get("//get_annotations") @@ -336,12 +336,12 @@ def get_annotations(doc_id: int, only_own: bool = False) -> Response: revset = {r.reviewer_id for r in peer_reviews} for ann in results: if ann.annotator.id != current_user.id and ann.annotator_id in revset: - ann.annotator.hide_name = True + ann.annotator.is_name_hidden = True for p in peer_reviews: if p.reviewer_id != current_user.id: - p.reviewer.hide_name = True + p.reviewer.is_name_hidden = True if p.reviewable_id != current_user: - p.reviewable.hide_name = True + p.reviewable.is_name_hidden = True return no_cache_json_response( {"annotations": results, "peer_reviews": peer_reviews}, date_conversion=True diff --git a/timApp/velp/annotation_model.py b/timApp/velp/annotation_model.py index 20527f263c..fbca9b3195 100644 --- a/timApp/velp/annotation_model.py +++ b/timApp/velp/annotation_model.py @@ -6,7 +6,8 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.user.user import User @@ -45,7 +46,7 @@ class AnnotationPosition: end: AnnotationCoordinate -class Annotation(DbModel): +class Annotation(db.Model): """An annotation that can be associated with an Answer or with a DocParagraph in a Document. The annotation can start and end in specific positions, in which case the annotation is supposed to be displayed diff --git a/timApp/velp/velp.py b/timApp/velp/velp.py index b26407ffe5..8a93f37725 100644 --- a/timApp/velp/velp.py +++ b/timApp/velp/velp.py @@ -820,6 +820,7 @@ def create_velp_group_route(doc_id: int) -> Response: else: # Copy all document rights to document velp group if target_type == 1: + assert isinstance(target, DocInfo) add_velp_group_perms(target.document.id, velp_group) else: raise RouteException(f"Could not find document or folder.") diff --git a/timApp/velp/velp_models.py b/timApp/velp/velp_models.py index f032907b36..46fa78e929 100644 --- a/timApp/velp/velp_models.py +++ b/timApp/velp/velp_models.py @@ -7,13 +7,14 @@ from sqlalchemy.orm.collections import attribute_mapped_collection # type: ignore from timApp.item.block import Block -from timApp.timdb.types import datetime_tz, DbModel +from timApp.timdb.sqa import db +from timApp.timdb.types import datetime_tz if TYPE_CHECKING: from timApp.user.user import User -class VelpContent(DbModel): +class VelpContent(db.Model): """The actual content of a Velp.""" version_id: Mapped[int] = mapped_column( @@ -26,7 +27,7 @@ class VelpContent(DbModel): velp_version: Mapped["VelpVersion"] = relationship() -class AnnotationComment(DbModel): +class AnnotationComment(db.Model): """A comment in an Annotation.""" id: Mapped[int] = mapped_column(primary_key=True) @@ -56,21 +57,21 @@ def to_json(self) -> dict: } -class LabelInVelp(DbModel): +class LabelInVelp(db.Model): """Associates VelpLabels with Velps.""" label_id: Mapped[int] = mapped_column(ForeignKey("velplabel.id"), primary_key=True) velp_id: Mapped[int] = mapped_column(ForeignKey("velp.id"), primary_key=True) -class VelpInGroup(DbModel): +class VelpInGroup(db.Model): velp_group_id: Mapped[int] = mapped_column( ForeignKey("velpgroup.id"), primary_key=True ) velp_id: Mapped[int] = mapped_column(ForeignKey("velp.id"), primary_key=True) -class Velp(DbModel): +class Velp(db.Model): """A Velp is a kind of category for Annotations and is visually represented by a Post-it note.""" id: Mapped[int] = mapped_column(primary_key=True) @@ -117,7 +118,7 @@ def to_json(self) -> dict: } -class VelpGroup(DbModel): +class VelpGroup(db.Model): """Represents a group of Velps.""" id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) @@ -143,7 +144,7 @@ def to_json(self) -> dict: } -class VelpGroupDefaults(DbModel): +class VelpGroupDefaults(db.Model): doc_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) target_id: Mapped[str] = mapped_column(primary_key=True) velp_group_id: Mapped[int] = mapped_column( @@ -153,14 +154,14 @@ class VelpGroupDefaults(DbModel): selected: Mapped[Optional[bool]] = mapped_column(default=False) -class VelpGroupLabel(DbModel): +class VelpGroupLabel(db.Model): """Currently not used (0 rows in production DB as of 5th July 2018).""" id: Mapped[int] = mapped_column(primary_key=True) content: Mapped[str] -class VelpGroupSelection(DbModel): +class VelpGroupSelection(db.Model): user_id: Mapped[int] = mapped_column(ForeignKey("useraccount.id"), primary_key=True) doc_id: Mapped[int] = mapped_column(ForeignKey("block.id"), primary_key=True) target_id: Mapped[str] = mapped_column(primary_key=True) @@ -171,7 +172,7 @@ class VelpGroupSelection(DbModel): ) -class VelpGroupsInDocument(DbModel): +class VelpGroupsInDocument(db.Model): """ TODO: This table contains lots of rows in production DB (about 19000 as of 5th July 2018). @@ -185,7 +186,7 @@ class VelpGroupsInDocument(DbModel): ) -class VelpLabel(DbModel): +class VelpLabel(db.Model): """A label that can be assigned to a Velp.""" id: Mapped[int] = mapped_column(primary_key=True) @@ -200,7 +201,7 @@ class VelpLabel(DbModel): ) -class VelpLabelContent(DbModel): +class VelpLabelContent(db.Model): velplabel_id: Mapped[int] = mapped_column( ForeignKey("velplabel.id"), primary_key=True ) @@ -217,7 +218,7 @@ def to_json(self) -> dict: } -class VelpVersion(DbModel): +class VelpVersion(db.Model): id: Mapped[int] = mapped_column(primary_key=True) velp_id: Mapped[int] = mapped_column(ForeignKey("velp.id")) modify_time: Mapped[datetime_tz] = mapped_column(default=datetime.utcnow) diff --git a/timApp/velp/velpgroups.py b/timApp/velp/velpgroups.py index 41c533ae52..548680f3b5 100644 --- a/timApp/velp/velpgroups.py +++ b/timApp/velp/velpgroups.py @@ -253,7 +253,7 @@ def make_document_a_velp_group( def add_groups_to_document( - velp_groups: list[VelpGroupOrDocInfo], doc: DocInfo, user: User + velp_groups: Sequence[VelpGroupOrDocInfo], doc: DocInfo, user: User ) -> None: """Adds velp groups to VelpGroupsInDocument table.""" existing: Sequence[VelpGroupsInDocument] = ( From 0d8a617242c2f82c2fd926ef15ced400c6e1b50c Mon Sep 17 00:00:00 2001 From: dezhidki Date: Fri, 15 Sep 2023 23:07:04 +0300 Subject: [PATCH 34/34] Ensure unzip is installed in tim container --- timApp/Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/timApp/Dockerfile b/timApp/Dockerfile index 69034ebded..2ade9995e8 100755 --- a/timApp/Dockerfile +++ b/timApp/Dockerfile @@ -176,8 +176,7 @@ RUN sed -i "s/from werkzeug import cached_property/from werkzeug.utils import ca RUN wget -q https://www.texlive.info/CTAN/support/latexmk/latexmk.pl -O /usr/bin/latexmk -RUN bash -c "${APT_INSTALL} jq && ${APT_CLEANUP}" -RUN bash -c "${APT_INSTALL} ripgrep && ${APT_CLEANUP}" +RUN bash -c "${APT_INSTALL} jq ripgrep unzip && ${APT_CLEANUP}" RUN bash -c "${PIP_INSTALL} coverage && ${APT_CLEANUP}" # Chromedriver (for running tests) @@ -186,7 +185,7 @@ ARG CHROME_VERSION="google-chrome-stable" RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ && echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list \ && apt-get update -qqy \ - && apt-get -qqy install unzip \ + && apt-get -qqy install \ ${CHROME_VERSION:-google-chrome-stable} \ && rm /etc/apt/sources.list.d/google-chrome.list \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/*