diff --git a/Pipfile b/Pipfile index a872e1184..c2db33487 100644 --- a/Pipfile +++ b/Pipfile @@ -57,6 +57,7 @@ watchdog = "~=4.0" whitenoise = "~=6.7" whoosh = "~=2.7" zxing-cpp = {version = "*", platform_machine = "== 'x86_64'"} +jinja2 = "~=3.1" [dev-packages] # Linting diff --git a/Pipfile.lock b/Pipfile.lock index 47622a94f..179dbfec0 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1be8ddf875b6aa77fcf61f5c065c9dc3941cad4b9285ce64da60b5684357dade" + "sha256": "1e113d0879e4e0bc3c384115057647ac8d9be05252dd7c708a1fc873f294ef28" }, "pipfile-spec": 6, "requires": {}, @@ -544,12 +544,12 @@ }, "django-soft-delete": { "hashes": [ - "sha256:428df56ea4fbb13f42d4f752f11f2a517aa31ac3d1b450e6b78c4c5d5d9dfc3b", - "sha256:558821ea988fd69a3a7008cdb33a06ded491af828bdffa5b287fa0fb72b52a09" + "sha256:36cf26a9eaa5f4c0fdb5cb6367ea183e91b7f73783cad173e4071a4747dd1277", + "sha256:fc16c870020984b7f58254adead12fdfb637a6c2f4bd8a93a3a636b18b1463e0" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==1.0.14" + "version": "==1.0.15" }, "djangorestframework": { "hashes": [ @@ -744,11 +744,11 @@ }, "httpcore": { "hashes": [ - "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", - "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5" + "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", + "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" ], "markers": "python_version >= '3.8'", - "version": "==1.0.5" + "version": "==1.0.6" }, "httptools": { "hashes": [ @@ -828,11 +828,11 @@ }, "imap-tools": { "hashes": [ - "sha256:218ea6495d73275ecc2fa4a34717c137bacf2c4a3d34c9d10a9581a6af1ac94f", - "sha256:4c31e9df1d28149436a86871cf84a0b37221a91521fc1a57897e0a152ee3f6d1" + "sha256:bd84d0f40fbd7be27f6ff5c3908e74d96e99d6b5f44f19cd6e928d308c811916", + "sha256:e657df2f62c1b263c0fd1610cfcd9f8cde26de6b696ae25c401ba75d91a5fd93" ], "index": "pypi", - "version": "==1.7.2" + "version": "==1.7.3" }, "img2pdf": { "hashes": [ @@ -844,7 +844,7 @@ "hashes": [ "sha256:8440ffe49c4ae81a8df57c1ae1eb4b6bfa7acb830099bfb3e305b383005cc128" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1'", "version": "==1.3.5" }, "inotifyrecursive": { @@ -853,9 +853,18 @@ "sha256:a2c450b317693e4538416f90eb1d7858506dafe6b8b885037bd2dd9ae2dafa1e" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1'", "version": "==0.3.5" }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, "joblib": { "hashes": [ "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", @@ -1032,6 +1041,72 @@ "markers": "python_version >= '3.8'", "version": "==3.0.0" }, + "markupsafe": { + "hashes": [ + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.5" + }, "mdurl": { "hashes": [ "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", @@ -1304,54 +1379,49 @@ }, "pikepdf": { "hashes": [ - "sha256:01be001988ce0f6a5a89319f37fc14f27df75c4e332222ed8e993d14405acb02", - "sha256:0759842e47369fe5fa0d61de2ac9ff073895c75567f3efbc4aebc6c1cafee17e", - "sha256:127e94632eb1ccd5d4d859511f084a0a314555cba621595a135915fc9e1710c5", - "sha256:163600dcd8d158e9287934b65a516b469b153859ab029e40fb3a0eff16c7dd7a", - "sha256:1dd707e6159af953f5560138f695b3a1ae2e1a0750535be70a3b75a720279330", - "sha256:1e6b3083ef2e3c29af33fcdb73a9a61a8e4dbe540edb474c19b9866194c6bf25", - "sha256:3c7e5c3a425de7db1fc13583883d2fa10119ce85071cc1d53344383498739254", - "sha256:3efff6ffda819d4193dd8e63c6f304bf85f9ae961c0247dc0b716b7c74fb7094", - "sha256:4a5c5ccccb5812a5be5b5cb66c8c8a6f796910ab89932a3048a4e66e5436bd01", - "sha256:4b9e9416da42da43f386244b2bab2a236830ccb11598b73fcd43d32fd234aaff", - "sha256:4c8bf24b8bf933f4022c6ace5ee757453e3dacb806a8e826461fd5f33ce15a70", - "sha256:531b6685912eb630a7fe57c527c9b5636c50c543eb0cdb5807b139e0d7712696", - "sha256:5e31aeb15ab21ba340a9013c1665e7ce85bd1f8167e6710c455d51f82c2e64e0", - "sha256:61bb9dfe58ee3ee2a286ea4cd21af87e1853a2d1433b550e3f58faa005b6ea3a", - "sha256:6275467b7eacb6fb04f16727e90e6562c6bbf449ece4e57273956beb8f1cdacd", - "sha256:6e15689fd715e83ff555cbdb939a0453c6c94af9975ae9b3292dd68231014653", - "sha256:755f559c206de5b3de0e35430ad28e50f37866d96a41b3ad41d7114660e1c58b", - "sha256:7fa15e5ff3e17dc6295d676d673787c79fec67cca59261a22ccf7604914170b1", - "sha256:8a50c58bee394f69561ab2861f77ce763f91cf7af6c8a1919109bb33fe8ca669", - "sha256:9699fe058b44e59cdcd05bcadf9cfa8f5242b48e44f9a4772bb321cd74d8e339", - "sha256:96ea92374d25481a2213403ae06c990ea41a1f35b0404dd072b7070dac76f41b", - "sha256:98ff348c97c7c641c2d2b741d60c8edf22e0fe76fa5c386cb351a3abd3f2a9b9", - "sha256:a32ef219737e53b48754acb45ad7840aee8403d97fc79539c26501a2d9089c91", - "sha256:aefa94f8ea6371fc3cbf78f55f669efec6e28e317927e8dd8a237e19a7be50fb", - "sha256:baaf78ed49e3cecfc4d30f2c7291d9b19bebe8a5f8e5940d7e7c93683b47a6f9", - "sha256:c1b883e1ebe28fbc318ce5c971b3dca9b30621bc2fe1642c99cda76cf442c4a2", - "sha256:c2c21c6a3d7ec96c7f9627ad61195eadff12659e3e00abe7156c34503189db47", - "sha256:c4eb22efae62b057a31ee4cb5574db8edfe15b185c8e89500eca8157fda15974", - "sha256:c6ea5f623629478abaf1e25b1d0edcaee3d0408fd9061fb4f7dc24fb78a25302", - "sha256:cd73d828799e41ee778606e30efd0c27be1e2420b1ed0c9cbc39299872ceed76", - "sha256:ceeac42bfb7227310e617e871d8f7ae6f304cf2783ca0131f3063c54ee1ecb73", - "sha256:d1a1314e4c4b2a28a1af1e700570b3c32c074cf363425768e8bc9f031438aee3", - "sha256:d209e4a9ba99a4460cf987f6cd8703a8723d8a62fc51451c4c1233eff07db02f", - "sha256:d360e64c31f73b16b78ca1e10e9d96f758b4a3fac195cd35f88a5f213808852e", - "sha256:d37ce8a4ade0cddf3827e13867208ffc8c161d38fdb12250b31e1b8cfa58ab1b", - "sha256:d6f240b0c1da5b6656efa3daa087394ddce5b3ecc411b85efcfd7e7228a1bc26", - "sha256:d9ba6c639faac47a85817854d002e2f57683ffe65388a746af580c4a6521646c", - "sha256:e199833ef11a64f22945a9a98d56a98968e988e407cb20d9fa8b6081075c9604", - "sha256:e1e47e80ecfd77dbfc6c7e807e78e5cce0c10d5bd7804c0d9064429d72af981c", - "sha256:e863185d6abadab140a7c3e152d9227afe495cf97d4738efc280896660249180", - "sha256:eb65a84fff25295707250b49f9e2d1186e9f6b4b7f828a0d9e7e2b65a7af6311", - "sha256:f2e4d5632dc03a41d901e4feee474557145c4906d96cf6e7ae8106a85142d2eb", - "sha256:f3ecbc250254b61de2ca973e3d57acb07720e5a810ee0c81d33b051c76d22208", - "sha256:f6b1ee86850fddaea15afdde394109332f7dc63a156e52fb131f9b647b16f920", - "sha256:fc0deac6dd356ef95fcf42db917cfe2c5375640295609924d4825052c2124509" + "sha256:08d0c72ba70cbe9f45772168e0c922b8d7625899cbfbcbd0dfd1316acff90258", + "sha256:0da5ebba4a31e257ca86a93657a4d47afffeda2ee48cde25227ce43d6dabae13", + "sha256:0f74ba40a3c6f450d19b0958df5c92f84965f4160fd973d4a00f00492093f01c", + "sha256:180e7423f3b517688cf14d6c5537e97a1a9b047421915bb28d3198f881b46f14", + "sha256:18e48cc0359f29b5083bad94237b53d928d8491f7ba5d4a389ca5c366226d766", + "sha256:287206055d2543ee768f85c24146e267c2465c1b2024e37ccf80b5a16674d2a2", + "sha256:344602b23ae6852180587c8e3280719ac31c78a4ca6cf08d8a51467d5f1741ba", + "sha256:363d01aa89f871c12fdc3d08c677456d693028cfb865e314cebe679273a7ebcb", + "sha256:38b3f882351d17f65d38d43d24772cfe471b63dc8c09dad52434c4fe02693e33", + "sha256:3afa0ea7b57a125a7744313b08062e59ecca15b2b3b31d13431244ec99b4d683", + "sha256:3ffc14ad4172f7acd7c1c7eb22eeac66f92c93c83941c63a3b56961602af67d7", + "sha256:40724cb905ce682c97f048e4eb3a728eade6dd1bc64425f3b7bb9872688964ea", + "sha256:4a56b7ccf13817689adb977ba92efa8d567d42a307154acff156179ddb76668b", + "sha256:53202d816838e87ee80c28af695b554e3cbfd5cb3598d7bcfba533f9dbd411e9", + "sha256:58e256aec46ee13256e264bae949e23a98707833fc27a3e3c7172c034d0ab870", + "sha256:5eef37caae6ad7a4baa4a6cdb35690945ee1a83bc0da5bbbf0023bc27d113f9c", + "sha256:663ddb129d823f9e1d1e5b4118906c508b801bf1d86fd8583938f96588bf8dda", + "sha256:689fcd1e89857ddc31191d4cc7a1fab2dbb5ce88c347f4de0db41abb176a11fb", + "sha256:6b905b05fc32c4e279aceb1578d7d917ed9a4e70a8a8e8d1b40ee8afff9d6bfc", + "sha256:7a9a738186b07a1177369713e8003371d0393808e5a62b2af86751dad6684a92", + "sha256:7a9feafdb688e64e4017b4596c3cf90793cd658b53e915e6c5a2668d1b3eb0c9", + "sha256:7ac65c0ace97d995dc7263d2912208ac5310c2f84f42f1fdf043b47d77c01852", + "sha256:7dd4166bb14db7d0711f2a32b21cf479217e34828af435b7ece0fab6ea02664d", + "sha256:8022a925cb2c67a1de3736c19de5d280d43241e1b118f1188b94df07e84c8b8f", + "sha256:80630a897d4203be10861e4e7fca8774cf1a85a1abcc41f978984564fb729ef6", + "sha256:8422a3944187a8d24626812044b6b09c865426e2bf8d0b2ead80f56f609b3345", + "sha256:84555d4039ea10935fa2d0084577de5b81b508b9716ce482163e2dc65db1b180", + "sha256:8dbab43c6a6fa2737df6cfccd049bbe5b762c39809a0b14484d0154f403be4fb", + "sha256:8f1153d3f7be818ba0f9f0875f37ed5203c3d500c33a4058a4d2d0f978d3ce29", + "sha256:906d8afc1aa4f2f7409381a58e158207170f3aeba8ad2aec40072a648e8a2914", + "sha256:98e546120b0d5707836a5ced43b09c086f5866f6eed93cfe4a0555c987fcba6f", + "sha256:9e5bb5e40394d6a15c494469be5026c063676918cbabf48345c7fdf8b2f776f5", + "sha256:a09688758168a86585bb0baeae0a704349285ef40a02da8739be4ad8f4b1aee7", + "sha256:cd796a039cbaddb6106127f210d5f2160654c0e629c1b663f2d9e6f67bba96b8", + "sha256:d0d6b11da16d280f83c5406ae0db03521e613c7758212b9104bad3dbf9bf2098", + "sha256:d96804a7e26e2ff37a9c2d796042754b7cae0668ed118a9185169fe1fc3b18d6", + "sha256:dbe7d9930789ea56e8b38b3b6b2b0b4e1090509825ceb572b906a1d23dea0282", + "sha256:e6bb3466f92b7a741a58fe348285d7bec69ea6102bbe3b2a3f49af0e6f2f3327", + "sha256:ecb8ab93305f07f806399101858ab9ff350c3e1de819d6043b5d54220cf81e71", + "sha256:f54ad2d6d3e4c564bf1f9c33e4165b4c36aea62c49654f356a5570f99b89c647" ], - "markers": "python_version >= '3.8'", - "version": "==9.2.1" + "markers": "python_version >= '3.9'", + "version": "==9.3.0" }, "pillow": { "hashes": [ @@ -1476,17 +1546,17 @@ "c" ], "hashes": [ - "sha256:8bad2e497ce22d556dac1464738cb948f8d6bab450d965cf1d8a8effd52412e0", - "sha256:babf565d459d8f72fb65da5e211dd0b58a52c51e4e1fa9cadecff42d6b7619b2" + "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907", + "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2" ], "markers": "python_version >= '3.8'", - "version": "==3.2.2" + "version": "==3.2.3" }, "psycopg-c": { "hashes": [ - "sha256:de8cac75bc6640ef0f54ad9187b81e07c430206a83c566b73d4cca41ecccb7c8" + "sha256:06ae7db8eaec1a3845960fa7f997f4ccdb1a7a7ab8dc593a680bcc74e1359671" ], - "version": "==3.2.2" + "version": "==3.2.3" }, "pycparser": { "hashes": [ @@ -1520,7 +1590,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "python-dotenv": { @@ -1732,108 +1802,17 @@ "hiredis" ], "hashes": [ - "sha256:b756df1e4a3858fcc0ef861f3fc53623a96c41e2b1f5304e09e0fe758d333d40", - "sha256:fd4fccba0d7f6aa48c58a78d76ddb4afc698f5da4a2c1d03d916e4fd7ab88cdd" + "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72", + "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24" ], "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.1.1" }, "regex": { "hashes": [ - "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", - "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", - "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", - "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", - "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", - "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", - "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", - "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", - "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", - "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", - "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", - "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", - "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", - "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", - "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", - "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", - "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", - "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", - "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", - "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", - "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", - "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", - "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", - "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", - "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", - "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", - "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", - "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", - "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", - "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", - "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", - "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", - "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", - "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", - "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", - "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", - "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", - "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", - "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", - "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", - "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", - "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", - "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", - "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", - "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", - "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", - "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", - "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", - "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", - "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", - "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", - "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", - "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", - "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", - "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", - "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", - "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", - "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", - "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", - "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", - "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", - "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", - "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", - "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", - "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", - "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", - "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", - "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", - "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", - "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", - "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", - "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", - "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", - "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", - "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", - "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", - "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", - "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", - "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", - "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", - "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", - "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", - "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", - "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", - "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", - "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", - "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", - "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", - "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", - "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", - "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", - "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919" + "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a" ], "markers": "python_version >= '3.8'", "version": "==2024.9.11" @@ -1843,7 +1822,6 @@ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.8'", "version": "==2.32.3" }, "requests-oauthlib": { @@ -1855,15 +1833,16 @@ }, "rich": { "hashes": [ - "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", - "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a" + "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c", + "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1" ], - "markers": "python_full_version >= '3.7.0'", - "version": "==13.8.1" + "markers": "python_full_version >= '3.8.0'", + "version": "==13.9.2" }, "scikit-learn": { "hashes": [ "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445", + "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3", "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de", "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6", "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0", @@ -1877,10 +1856,14 @@ "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6", "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9", "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540", + "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908", "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", + "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f", "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113", "sha256:ca64b3089a6d9b9363cd3546f8978229dcbb737aceb2c12144ee3f70f95684b7", + "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5", "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd", + "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12", "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675", "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1", "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a" @@ -2028,7 +2011,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sniffio": { @@ -3057,11 +3040,11 @@ }, "faker": { "hashes": [ - "sha256:bf0207af5777950054a2a3b43f4b5bdc33b585918d2b28f1dab52ac0ffe2bac0", - "sha256:f0a60009150736c1c033bea31aa19ae63071c9dcf10adfaf9f1a87a3add84bc8" + "sha256:dbf81295c948270a9e96cd48a9a3ebec73acac9a153d0c854fbbd0294557609f", + "sha256:e0593931bd7be9a9ea984b5d8c302ef1cec19392585d1e90d444199271d0a94d" ], "markers": "python_version >= '3.8'", - "version": "==30.0.0" + "version": "==30.1.0" }, "filelock": { "hashes": [ @@ -3089,11 +3072,11 @@ }, "httpcore": { "hashes": [ - "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", - "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5" + "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", + "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" ], "markers": "python_version >= '3.8'", - "version": "==1.0.5" + "version": "==1.0.6" }, "httpx": { "extras": [ @@ -3158,6 +3141,7 @@ "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" ], + "index": "pypi", "markers": "python_version >= '3.7'", "version": "==3.1.4" }, @@ -3519,11 +3503,11 @@ }, "pymdown-extensions": { "hashes": [ - "sha256:2653fb658bca5f278029f8c67a67f0f08b7bd3c657e2630d261ad542e97c4192", - "sha256:e68080eac44634406b31f4aec58fbad17b0ec5fca6b086e29008616d54c3906b" + "sha256:41cdde0a77290e480cf53892f5c5e50921a7ee3e5cd60ba91bf19837b33badcf", + "sha256:bc8847ecc9e784a098efd35e20cba772bc5a1b529dfcef9dc1972db9021a1049" ], "markers": "python_version >= '3.8'", - "version": "==10.11" + "version": "==10.11.2" }, "pyopenssl": { "hashes": [ @@ -3618,7 +3602,7 @@ "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pywavelets": { @@ -3731,100 +3715,9 @@ }, "regex": { "hashes": [ - "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", - "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", - "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", - "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", - "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", - "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", - "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", - "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", - "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", - "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", - "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", - "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", - "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", - "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", - "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", - "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", - "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", - "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", - "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", - "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", - "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", - "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", - "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", - "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", - "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", - "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", - "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", - "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", - "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", - "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", - "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", - "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", - "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", - "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", - "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", - "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", - "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", - "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", - "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", - "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", - "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", - "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", - "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", - "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", - "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", - "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", - "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", - "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", - "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", - "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", - "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", - "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", - "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", - "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", - "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", - "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", - "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", - "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", - "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", - "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", - "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", - "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", - "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", - "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", - "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", - "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", - "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", - "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", - "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", - "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", - "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", - "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", - "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", - "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", - "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", - "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", - "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", - "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", - "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", - "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", - "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", - "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", - "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", - "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", - "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", - "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", - "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", - "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", - "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", - "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", - "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", - "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919" + "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a" ], "markers": "python_version >= '3.8'", "version": "==2024.9.11" @@ -3834,33 +3727,32 @@ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.8'", "version": "==2.32.3" }, "ruff": { "hashes": [ - "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750", - "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa", - "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c", - "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0", - "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f", - "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098", - "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0", - "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f", - "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44", - "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2", - "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a", - "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc", - "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb", - "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18", - "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5", - "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce", - "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263", - "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87" + "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd", + "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0", + "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec", + "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7", + "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb", + "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5", + "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c", + "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625", + "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e", + "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117", + "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f", + "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829", + "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039", + "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa", + "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93", + "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2", + "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577", + "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.6.8" + "version": "==0.6.9" }, "scipy": { "hashes": [ @@ -3921,7 +3813,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sniffio": { @@ -3942,11 +3834,11 @@ }, "tomli": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], "markers": "python_version < '3.11'", - "version": "==2.0.1" + "version": "==2.0.2" }, "twisted": { "extras": [ @@ -4412,7 +4304,6 @@ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.8'", "version": "==2.32.3" }, "sqlparse": { @@ -4425,11 +4316,11 @@ }, "tomli": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], "markers": "python_version < '3.11'", - "version": "==2.0.1" + "version": "==2.0.2" }, "types-bleach": { "hashes": [ @@ -4468,11 +4359,11 @@ }, "types-docutils": { "hashes": [ - "sha256:5dd2aa5e2e06fcfa090020bc4115479b4dd28da3329ab708563ee29894bd3c0d", - "sha256:9c8ed6d90583944af00f6b5fa3aecc2101e20672f6b1a4a299c6bf7d1e47084d" + "sha256:0d2ea594576e8d05c4ad83165da64a511e538f6ab405ab8347cd6b636c59f934", + "sha256:9816fb4f33067ed22d24c776a411a430bc19318b1af8f373e5581702a07bc4bc" ], "markers": "python_version >= '3.8'", - "version": "==0.21.0.20240907" + "version": "==0.21.0.20241004" }, "types-html5lib": { "hashes": [ @@ -4519,12 +4410,12 @@ }, "types-python-dateutil": { "hashes": [ - "sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6", - "sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e" + "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d", + "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.9.0.20240906" + "version": "==2.9.0.20241003" }, "types-pyyaml": { "hashes": [ @@ -4536,12 +4427,12 @@ }, "types-redis": { "hashes": [ - "sha256:0e7537e5c085fe96b7d468d5edae0cf667b4ba4b62c6e4a5dfc340bd3b868c23", - "sha256:4bab1a378dbf23c2c95c370dfdb89a8f033957c4fd1a53fee71b529c182fe008" + "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", + "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.6.0.20240903" + "version": "==4.6.0.20241004" }, "types-requests": { "hashes": [ diff --git a/src/documents/checks.py b/src/documents/checks.py index ef6f4e845..a97c517aa 100644 --- a/src/documents/checks.py +++ b/src/documents/checks.py @@ -9,7 +9,7 @@ from django.db.utils import OperationalError from django.db.utils import ProgrammingError from documents.signals import document_consumer_declaration -from documents.templatetags.filepath import convert_to_django_template_format +from documents.templating.utils import convert_format_str_to_template_format @register() @@ -76,7 +76,9 @@ def parser_check(app_configs, **kwargs): @register() def filename_format_check(app_configs, **kwargs): if settings.FILENAME_FORMAT: - converted_format = convert_to_django_template_format(settings.FILENAME_FORMAT) + converted_format = convert_format_str_to_template_format( + settings.FILENAME_FORMAT, + ) if converted_format != settings.FILENAME_FORMAT: return [ Warning( diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index bbe91081f..6d02bf684 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -1,36 +1,10 @@ -import logging import os -import re -from collections.abc import Iterable -from pathlib import PurePath -import pathvalidate from django.conf import settings -from django.template import Context -from django.template import Engine -from django.utils import timezone -from documents.models import Correspondent -from documents.models import CustomField -from documents.models import CustomFieldInstance from documents.models import Document -from documents.models import DocumentType -from documents.models import StoragePath -from documents.models import Tag -from documents.templatetags.filepath import convert_to_django_template_format - -logger = logging.getLogger("paperless.filehandling") - -INVALID_VARIABLE_STR = "InvalidVarError" - -filepath_engine = Engine( - autoescape=False, - string_if_invalid=f"{INVALID_VARIABLE_STR}: %s", - libraries={ - "filepath": "documents.templatetags.filepath", - "get_cf_value": "documents.templatetags.get_cf_value", - }, -) +from documents.templating.filepath import validate_filepath_template_and_render +from documents.templating.utils import convert_format_str_to_template_format def create_source_path_directory(source_path): @@ -116,256 +90,6 @@ def generate_unique_filename(doc, archive_filename=False): return new_filename -def create_dummy_document(): - """ - Create a dummy Document instance with all possible fields filled - """ - # Populate the document with representative values for every field - dummy_doc = Document( - pk=1, - title="Sample Title", - correspondent=Correspondent(name="Sample Correspondent"), - storage_path=StoragePath(path="/dummy/path"), - document_type=DocumentType(name="Sample Type"), - content="This is some sample document content.", - mime_type="application/pdf", - checksum="dummychecksum12345678901234567890123456789012", - archive_checksum="dummyarchivechecksum123456789012345678901234", - page_count=5, - created=timezone.now(), - modified=timezone.now(), - storage_type=Document.STORAGE_TYPE_UNENCRYPTED, - added=timezone.now(), - filename="/dummy/filename.pdf", - archive_filename="/dummy/archive_filename.pdf", - original_filename="original_file.pdf", - archive_serial_number=12345, - ) - return dummy_doc - - -def get_creation_date_context(document: Document) -> dict[str, str]: - """ - Given a Document, localizes the creation date and builds a context dictionary with some common, shorthand - formatted values from it - """ - local_created = timezone.localdate(document.created) - - return { - "created": local_created.isoformat(), - "created_year": local_created.strftime("%Y"), - "created_year_short": local_created.strftime("%y"), - "created_month": local_created.strftime("%m"), - "created_month_name": local_created.strftime("%B"), - "created_month_name_short": local_created.strftime("%b"), - "created_day": local_created.strftime("%d"), - } - - -def get_added_date_context(document: Document) -> dict[str, str]: - """ - Given a Document, localizes the added date and builds a context dictionary with some common, shorthand - formatted values from it - """ - local_added = timezone.localdate(document.added) - - return { - "added": local_added.isoformat(), - "added_year": local_added.strftime("%Y"), - "added_year_short": local_added.strftime("%y"), - "added_month": local_added.strftime("%m"), - "added_month_name": local_added.strftime("%B"), - "added_month_name_short": local_added.strftime("%b"), - "added_day": local_added.strftime("%d"), - } - - -def get_basic_metadata_context( - document: Document, - *, - no_value_default: str, -) -> dict[str, str]: - """ - Given a Document, constructs some basic information about it. If certain values are not set, - they will be replaced with the no_value_default. - - Regardless of set or not, the values will be sanitized - """ - return { - "title": pathvalidate.sanitize_filename( - document.title, - replacement_text="-", - ), - "correspondent": pathvalidate.sanitize_filename( - document.correspondent.name, - replacement_text="-", - ) - if document.correspondent - else no_value_default, - "document_type": pathvalidate.sanitize_filename( - document.document_type.name, - replacement_text="-", - ) - if document.document_type - else no_value_default, - "asn": str(document.archive_serial_number) - if document.archive_serial_number - else no_value_default, - "owner_username": document.owner.username - if document.owner - else no_value_default, - "original_name": PurePath(document.original_filename).with_suffix("").name - if document.original_filename - else no_value_default, - "doc_pk": f"{document.pk:07}", - } - - -def get_tags_context(tags: Iterable[Tag]) -> dict[str, str | list[str]]: - """ - Given an Iterable of tags, constructs some context from them for usage - """ - return { - "tag_list": pathvalidate.sanitize_filename( - ",".join( - sorted(tag.name for tag in tags), - ), - replacement_text="-", - ), - # Assumed to be ordered, but a template could loop through to find what they want - "tag_name_list": [x.name for x in tags], - } - - -def get_custom_fields_context( - custom_fields: Iterable[CustomFieldInstance], -) -> dict[str, dict[str, dict[str, str]]]: - """ - Given an Iterable of CustomFieldInstance, builds a dictionary mapping the field name - to its type and value - """ - field_data = {"custom_fields": {}} - for field_instance in custom_fields: - type_ = pathvalidate.sanitize_filename( - field_instance.field.data_type, - replacement_text="-", - ) - # String types need to be sanitized - if field_instance.field.data_type in { - CustomField.FieldDataType.DOCUMENTLINK, - CustomField.FieldDataType.MONETARY, - CustomField.FieldDataType.STRING, - CustomField.FieldDataType.URL, - }: - value = pathvalidate.sanitize_filename( - field_instance.value, - replacement_text="-", - ) - elif ( - field_instance.field.data_type == CustomField.FieldDataType.SELECT - and field_instance.field.extra_data["select_options"] is not None - ): - options = field_instance.field.extra_data["select_options"] - value = pathvalidate.sanitize_filename( - options[int(field_instance.value)], - replacement_text="-", - ) - else: - value = field_instance.value - field_data["custom_fields"][ - pathvalidate.sanitize_filename( - field_instance.field.name, - replacement_text="-", - ) - ] = { - "type": type_, - "value": value, - } - return field_data - - -def validate_template_and_render( - template_string: str, - document: Document | None = None, -) -> str | None: - """ - Renders the given template string using either the given Document or using a dummy Document and data - - Returns None if the string is not valid or an error occurred, otherwise - """ - - def detect_undefined_variables(rendered_string: str) -> list[str] | None: - """ - Checks the rendered template for variables which were not defined/invalid and returns a - listing of them or None if none were found. - - Used to provide context to the user, rather than mostly failing silently - - """ - pattern = rf"{INVALID_VARIABLE_STR}: ([\w]+(?:[.\s]+[\w]+)*)" - matches = re.findall(pattern, rendered_string) - - if matches: - return list(set(matches)) - else: - return None - - # Create the dummy document object with all fields filled in for validation purposes - if document is None: - document = create_dummy_document() - tags_list = [Tag(name="Test Tag 1"), Tag(name="Another Test Tag")] - custom_fields = [ - CustomFieldInstance( - field=CustomField( - name="Text Custom Field", - data_type=CustomField.FieldDataType.STRING, - ), - value_text="Some String Text", - ), - ] - else: - # or use the real document information - tags_list = document.tags.order_by("name").all() - custom_fields = document.custom_fields.all() - - # Build the context dictionary - context = ( - {"document": document} - | get_basic_metadata_context(document, no_value_default="-none-") - | get_creation_date_context(document) - | get_added_date_context(document) - | get_tags_context(tags_list) - | get_custom_fields_context(custom_fields) - ) - - # Try rendering the template - try: - # We load the custom tag used to remove spaces and newlines from the final string around the user string - template = filepath_engine.from_string( - "{% load filepath %}{% load get_cf_value %}{% filepath %}" - + template_string - + "{% endfilepath %}", - ) - rendered_template = template.render(Context(context)) - - # Check for errors - undefined_vars = detect_undefined_variables(rendered_template) - if undefined_vars: - logger.error(f"Template contained {len(undefined_vars)} undefined values:") - for x in undefined_vars: - logger.error(f" Variable '{x}' was undefined") - return None - - # We're good! - return rendered_template - except Exception as e: - logger.warning(f"Error in filename generation: {e}") - logger.warning( - f"Invalid filename_format '{template_string}', falling back to default", - ) - return None - - def generate_filename( doc: Document, counter=0, @@ -375,7 +99,10 @@ def generate_filename( path = "" def format_filename(document: Document, template_str: str) -> str | None: - rendered_filename = validate_template_and_render(template_str, document) + rendered_filename = validate_filepath_template_and_render( + template_str, + document, + ) if rendered_filename is None: return None @@ -394,14 +121,10 @@ def generate_filename( # Determine the source of the format string if doc.storage_path is not None: - logger.debug( - f"Document has storage_path {doc.storage_path.pk} " - f"({doc.storage_path.path}) set", - ) filename_format = doc.storage_path.path elif settings.FILENAME_FORMAT is not None: # Maybe convert old to new style - filename_format = convert_to_django_template_format( + filename_format = convert_format_str_to_template_format( settings.FILENAME_FORMAT, ) else: diff --git a/src/documents/migrations/1055_alter_storagepath_path.py b/src/documents/migrations/1055_alter_storagepath_path.py index 134a26c7f..cfa581b5d 100644 --- a/src/documents/migrations/1055_alter_storagepath_path.py +++ b/src/documents/migrations/1055_alter_storagepath_path.py @@ -10,11 +10,11 @@ def convert_from_format_to_template(apps, schema_editor): StoragePath = apps.get_model("documents", "StoragePath") - from documents.templatetags.filepath import convert_to_django_template_format + from documents.templating.utils import convert_format_str_to_template_format with transaction.atomic(): for storage_path in StoragePath.objects.all(): - storage_path.path = convert_to_django_template_format(storage_path.path) + storage_path.path = convert_format_str_to_template_format(storage_path.path) storage_path.save() diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index bb7028e76..c787f8456 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -34,7 +34,6 @@ if settings.AUDIT_LOG_ENABLED: from documents import bulk_edit from documents.data_models import DocumentSource -from documents.file_handling import validate_template_and_render from documents.models import Correspondent from documents.models import CustomField from documents.models import CustomFieldInstance @@ -54,7 +53,8 @@ from documents.models import WorkflowTrigger from documents.parsers import is_mime_type_supported from documents.permissions import get_groups_with_only_permission from documents.permissions import set_permissions_for_object -from documents.templatetags.filepath import convert_to_django_template_format +from documents.templating.filepath import validate_filepath_template_and_render +from documents.templating.utils import convert_format_str_to_template_format from documents.validators import uri_validator logger = logging.getLogger("paperless.serializers") @@ -1485,12 +1485,12 @@ class StoragePathSerializer(MatchingModelSerializer, OwnedObjectSerializer): ) def validate_path(self, path: str): - converted_path = convert_to_django_template_format(path) + converted_path = convert_format_str_to_template_format(path) if converted_path != path: logger.warning( f"Storage path {path} is not using the new style format, consider updating", ) - result = validate_template_and_render(converted_path) + result = validate_filepath_template_and_render(converted_path) if result is None: raise serializers.ValidationError(_("Invalid variable detected.")) diff --git a/src/documents/templatetags/filepath.py b/src/documents/templatetags/filepath.py deleted file mode 100644 index ff9b903d4..000000000 --- a/src/documents/templatetags/filepath.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import re - -from django import template - -register = template.Library() - - -class FilePathNode(template.Node): - """ - A custom tag to remove extra spaces before and after / as well as remove - any newlines from the resulting string. - - https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#parsing-until-another-block-tag - """ - - def __init__(self, nodelist): - self.nodelist = nodelist - - def render(self, context): - def clean_filepath(value): - """ - Clean up a filepath by: - 1. Removing newlines and carriage returns - 2. Removing extra spaces before and after forward slashes - 3. Preserving spaces in other parts of the path - """ - value = value.replace("\n", "").replace("\r", "") - value = re.sub(r"\s*/\s*", "/", value) - - # We remove trailing and leading separators, as these are always relative paths, not absolute, even if the user - # tries - return value.strip().strip(os.sep) - - output = self.nodelist.render(context) - return clean_filepath(output) - - -@register.tag(name="filepath") -def construct_filepath(parser, token): - """ - The registered tag as {% filepath %}, which is always loaded around the user provided template string to - render everything as a single line, with minimal spaces - """ - nodelist = parser.parse(("endfilepath",)) - parser.delete_first_token() - return FilePathNode(nodelist) - - -def convert_to_django_template_format(old_format: str) -> str: - """ - Converts old Python string format (with {}) to Django template style (with {{ }}), - while ignoring existing {{ ... }} placeholders. - - :param old_format: The old style format string (e.g., "{title} by {author}") - :return: Converted string in Django Template style (e.g., "{{ title }} by {{ author }}") - """ - - # Step 1: Match placeholders with single curly braces but not those with double braces - pattern = r"(? str: + def clean_filepath(value: str) -> str: + """ + Clean up a filepath by: + 1. Removing newlines and carriage returns + 2. Removing extra spaces before and after forward slashes + 3. Preserving spaces in other parts of the path + """ + value = value.replace("\n", "").replace("\r", "") + value = re.sub(r"\s*/\s*", "/", value) + + # We remove trailing and leading separators, as these are always relative paths, not absolute, even if the user + # tries + return value.strip().strip(os.sep) + + original_render = super().render(*args, **kwargs) + + return clean_filepath(original_render) + + +def get_cf_value( + custom_field_data: dict[str, dict[str, str]], + name: str, + default: str | None = None, +) -> str | None: + if name in custom_field_data: + return custom_field_data[name]["value"] + elif default is not None: + return default + return None + + +_template_environment.filters["get_cf_value"] = get_cf_value + + +def create_dummy_document(): + """ + Create a dummy Document instance with all possible fields filled + """ + # Populate the document with representative values for every field + dummy_doc = Document( + pk=1, + title="Sample Title", + correspondent=Correspondent(name="Sample Correspondent"), + storage_path=StoragePath(path="/dummy/path"), + document_type=DocumentType(name="Sample Type"), + content="This is some sample document content.", + mime_type="application/pdf", + checksum="dummychecksum12345678901234567890123456789012", + archive_checksum="dummyarchivechecksum123456789012345678901234", + page_count=5, + created=timezone.now(), + modified=timezone.now(), + storage_type=Document.STORAGE_TYPE_UNENCRYPTED, + added=timezone.now(), + filename="/dummy/filename.pdf", + archive_filename="/dummy/archive_filename.pdf", + original_filename="original_file.pdf", + archive_serial_number=12345, + ) + return dummy_doc + + +def get_creation_date_context(document: Document) -> dict[str, str]: + """ + Given a Document, localizes the creation date and builds a context dictionary with some common, shorthand + formatted values from it + """ + local_created = timezone.localdate(document.created) + + return { + "created": local_created.isoformat(), + "created_year": local_created.strftime("%Y"), + "created_year_short": local_created.strftime("%y"), + "created_month": local_created.strftime("%m"), + "created_month_name": local_created.strftime("%B"), + "created_month_name_short": local_created.strftime("%b"), + "created_day": local_created.strftime("%d"), + } + + +def get_added_date_context(document: Document) -> dict[str, str]: + """ + Given a Document, localizes the added date and builds a context dictionary with some common, shorthand + formatted values from it + """ + local_added = timezone.localdate(document.added) + + return { + "added": local_added.isoformat(), + "added_year": local_added.strftime("%Y"), + "added_year_short": local_added.strftime("%y"), + "added_month": local_added.strftime("%m"), + "added_month_name": local_added.strftime("%B"), + "added_month_name_short": local_added.strftime("%b"), + "added_day": local_added.strftime("%d"), + } + + +def get_basic_metadata_context( + document: Document, + *, + no_value_default: str, +) -> dict[str, str]: + """ + Given a Document, constructs some basic information about it. If certain values are not set, + they will be replaced with the no_value_default. + + Regardless of set or not, the values will be sanitized + """ + return { + "title": pathvalidate.sanitize_filename( + document.title, + replacement_text="-", + ), + "correspondent": pathvalidate.sanitize_filename( + document.correspondent.name, + replacement_text="-", + ) + if document.correspondent + else no_value_default, + "document_type": pathvalidate.sanitize_filename( + document.document_type.name, + replacement_text="-", + ) + if document.document_type + else no_value_default, + "asn": str(document.archive_serial_number) + if document.archive_serial_number + else no_value_default, + "owner_username": document.owner.username + if document.owner + else no_value_default, + "original_name": PurePath(document.original_filename).with_suffix("").name + if document.original_filename + else no_value_default, + "doc_pk": f"{document.pk:07}", + } + + +def get_tags_context(tags: Iterable[Tag]) -> dict[str, str | list[str]]: + """ + Given an Iterable of tags, constructs some context from them for usage + """ + return { + "tag_list": pathvalidate.sanitize_filename( + ",".join( + sorted(tag.name for tag in tags), + ), + replacement_text="-", + ), + # Assumed to be ordered, but a template could loop through to find what they want + "tag_name_list": [x.name for x in tags], + } + + +def get_custom_fields_context( + custom_fields: Iterable[CustomFieldInstance], +) -> dict[str, dict[str, dict[str, str]]]: + """ + Given an Iterable of CustomFieldInstance, builds a dictionary mapping the field name + to its type and value + """ + field_data = {"custom_fields": {}} + for field_instance in custom_fields: + type_ = pathvalidate.sanitize_filename( + field_instance.field.data_type, + replacement_text="-", + ) + # String types need to be sanitized + if field_instance.field.data_type in { + CustomField.FieldDataType.DOCUMENTLINK, + CustomField.FieldDataType.MONETARY, + CustomField.FieldDataType.STRING, + CustomField.FieldDataType.URL, + }: + value = pathvalidate.sanitize_filename( + field_instance.value, + replacement_text="-", + ) + elif ( + field_instance.field.data_type == CustomField.FieldDataType.SELECT + and field_instance.field.extra_data["select_options"] is not None + ): + options = field_instance.field.extra_data["select_options"] + value = pathvalidate.sanitize_filename( + options[int(field_instance.value)], + replacement_text="-", + ) + else: + value = field_instance.value + field_data["custom_fields"][ + pathvalidate.sanitize_filename( + field_instance.field.name, + replacement_text="-", + ) + ] = { + "type": type_, + "value": value, + } + return field_data + + +def validate_filepath_template_and_render( + template_string: str, + document: Document | None = None, +) -> str | None: + """ + Renders the given template string using either the given Document or using a dummy Document and data + + Returns None if the string is not valid or an error occurred, otherwise + """ + + # Create the dummy document object with all fields filled in for validation purposes + if document is None: + document = create_dummy_document() + tags_list = [Tag(name="Test Tag 1"), Tag(name="Another Test Tag")] + custom_fields = [ + CustomFieldInstance( + field=CustomField( + name="Text Custom Field", + data_type=CustomField.FieldDataType.STRING, + ), + value_text="Some String Text", + ), + ] + else: + # or use the real document information + tags_list = document.tags.order_by("name").all() + custom_fields = document.custom_fields.all() + + # Build the context dictionary + context = ( + {"document": document} + | get_basic_metadata_context(document, no_value_default="-none-") + | get_creation_date_context(document) + | get_added_date_context(document) + | get_tags_context(tags_list) + | get_custom_fields_context(custom_fields) + ) + + # Try rendering the template + try: + # We load the custom tag used to remove spaces and newlines from the final string around the user string + template = _template_environment.from_string( + template_string, + template_class=FilePathTemplate, + ) + rendered_template = template.render(context) + + # We're good! + return rendered_template + except UndefinedError: + # The undefined class logs this already for us + pass + except TemplateSyntaxError as e: + logger.warning(f"Template syntax error in filename generation: {e}") + except SecurityError as e: + logger.warning(f"Template attempted restricted operation: {e}") + except Exception as e: + logger.warning(f"Unknown error in filename generation: {e}") + logger.warning( + f"Invalid filename_format '{template_string}', falling back to default", + ) + return None diff --git a/src/documents/templating/utils.py b/src/documents/templating/utils.py new file mode 100644 index 000000000..894fda0f4 --- /dev/null +++ b/src/documents/templating/utils.py @@ -0,0 +1,24 @@ +import re + + +def convert_format_str_to_template_format(old_format: str) -> str: + """ + Converts old Python string format (with {}) to Jinja2 template style (with {{ }}), + while ignoring existing {{ ... }} placeholders. + + :param old_format: The old style format string (e.g., "{title} by {author}") + :return: Converted string in Django Template style (e.g., "{{ title }} by {{ author }}") + """ + + # Step 1: Match placeholders with single curly braces but not those with double braces + pattern = r"(?> is not safely callable", ) def test_template_with_custom_fields(self): + """ + GIVEN: + - Filename format which accesses custom field data + WHEN: + - Filepath for a document with this format is called + THEN: + - The custom field data is rendered + - If the field name is not defined, the default value is rendered, if any + """ doc_a = Document.objects.create( title="Some Title", created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), @@ -1260,6 +1316,18 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): data_type=CustomField.FieldDataType.INT, ) + cf2 = CustomField.objects.create( + name="Select Field", + data_type=CustomField.FieldDataType.SELECT, + extra_data={"select_options": ["ChoiceOne", "ChoiceTwo"]}, + ) + + CustomFieldInstance.objects.create( + document=doc_a, + field=cf2, + value_select=0, + ) + cfi = CustomFieldInstance.objects.create( document=doc_a, field=cf, @@ -1281,7 +1349,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): with override_settings( FILENAME_FORMAT=""" {% if "Invoice" in custom_fields %} - invoices/{{ custom_fields|get_cf_value:'Invoice' }} + invoices/{{ custom_fields | get_cf_value('Invoice') }} {% else %} not-invoices/{{ title }} {% endif %} @@ -1312,28 +1380,32 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase): cf.save() with override_settings( - FILENAME_FORMAT="invoices/{{ custom_fields|get_cf_value:'Invoice Number' }}", + FILENAME_FORMAT="invoices/{{ custom_fields | get_cf_value('Invoice Number') }}", ): self.assertEqual( generate_filename(doc_a), "invoices/4567.pdf", ) - def test_using_other_filters(self): - doc_a = Document.objects.create( - title="Some Title", - created=timezone.make_aware(datetime.datetime(2020, 6, 25, 7, 36, 51, 153)), - added=timezone.make_aware(datetime.datetime(2024, 10, 1, 7, 36, 51, 153)), - mime_type="application/pdf", - pk=2, - checksum="2", - archive_serial_number=25, - ) - with override_settings( - FILENAME_FORMAT="{% if document.correspondent %}{{ document.correspondent.name }}{% else %}No Correspondent{% endif %}/{title}", + FILENAME_FORMAT="invoices/{{ custom_fields | get_cf_value('Ince Number', 0) }}", ): self.assertEqual( generate_filename(doc_a), - "No Correspondent/Some Title.pdf", + "invoices/0.pdf", + ) + + + with override_settings( + FILENAME_FORMAT=""" + {% if "Select Field" in custom_fields %} + {{ title }}_{{ custom_fields|get_cf_value:'Select Field' }} + {% else %} + {{ title }} + {% endif %} + """, + ): + self.assertEqual( + generate_filename(doc_a), + "Some Title_ChoiceOne.pdf", )