diff --git a/Pipfile b/Pipfile index 162a175c..2760f828 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,7 @@ service_identity = "~=18.1.0" social-auth-app-django = "~=4.0.0" sqlparse = "~=0.4.2" pyopenssl = "*" +django-celery-beat = "*" [dev-packages] flake8 = "~=3.9.2" diff --git a/Pipfile.lock b/Pipfile.lock index 458489f3..44c0137a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "faf6dcb98cc0a6a1d4d6e3648d84b2ec8471ab6c9e258dc754027a5f0034389f" + "sha256": "bc83ce8df76eb8a70bafd44d392b2b34f0667e25f911c5387fa8cc7292b6d45a" }, "pipfile-spec": 6, "requires": {}, @@ -31,11 +31,11 @@ }, "asgiref": { "hashes": [ - "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e", - "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed" + "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", + "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" ], - "markers": "python_version >= '3.7'", - "version": "==3.7.2" + "markers": "python_version >= '3.8'", + "version": "==3.8.1" }, "asttokens": { "hashes": [ @@ -103,7 +103,7 @@ "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac", "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2" ], - "markers": "python_version < '3.9'", + "markers": "python_version >= '3.6'", "version": "==0.2.1" }, "billiard": { @@ -124,11 +124,11 @@ }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", + "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.7.4" }, "cffi": { "hashes": [ @@ -312,11 +312,11 @@ }, "click-didyoumean": { "hashes": [ - "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667", - "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035" + "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", + "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c" ], - "markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'", - "version": "==0.3.0" + "markers": "python_full_version >= '3.6.2'", + "version": "==0.3.1" }, "click-plugins": { "hashes": [ @@ -341,43 +341,50 @@ "markers": "python_version >= '3.8'", "version": "==23.10.4" }, + "cron-descriptor": { + "hashes": [ + "sha256:7b1a00d7d25d6ae6896c0da4457e790b98cba778398a3d48e341e5e0d33f0488", + "sha256:a67ba21804983b1427ed7f3e1ec27ee77bf24c652b0430239c268c5ddfbf9dc0" + ], + "version": "==1.4.3" + }, "cryptography": { "hashes": [ - "sha256:04859aa7f12c2b5f7e22d25198ddd537391f1695df7057c8700f71f26f47a129", - "sha256:069d2ce9be5526a44093a0991c450fe9906cdf069e0e7cd67d9dee49a62b9ebe", - "sha256:0d3ec384058b642f7fb7e7bff9664030011ed1af8f852540c76a1317a9dd0d20", - "sha256:0fab2a5c479b360e5e0ea9f654bcebb535e3aa1e493a715b13244f4e07ea8eec", - "sha256:0fea01527d4fb22ffe38cd98951c9044400f6eff4788cf52ae116e27d30a1ba3", - "sha256:1b797099d221df7cce5ff2a1d272761d1554ddf9a987d3e11f6459b38cd300fd", - "sha256:1e935c2900fb53d31f491c0de04f41110351377be19d83d908c1fd502ae8daa5", - "sha256:20100c22b298c9eaebe4f0b9032ea97186ac2555f426c3e70670f2517989543b", - "sha256:20180da1b508f4aefc101cebc14c57043a02b355d1a652b6e8e537967f1e1b46", - "sha256:25b09b73db78facdfd7dd0fa77a3f19e94896197c86e9f6dc16bce7b37a96504", - "sha256:2619487f37da18d6826e27854a7f9d4d013c51eafb066c80d09c63cf24505306", - "sha256:2eb6368d5327d6455f20327fb6159b97538820355ec00f8cc9464d617caecead", - "sha256:35772a6cffd1f59b85cb670f12faba05513446f80352fe811689b4e439b5d89e", - "sha256:39d5c93e95bcbc4c06313fc6a500cee414ee39b616b55320c1904760ad686938", - "sha256:3d96ea47ce6d0055d5b97e761d37b4e84195485cb5a38401be341fabf23bc32a", - "sha256:4dcab7c25e48fc09a73c3e463d09ac902a932a0f8d0c568238b3696d06bf377b", - "sha256:5fbf0f3f0fac7c089308bd771d2c6c7b7d53ae909dce1db52d8e921f6c19bb3a", - "sha256:6c25e1e9c2ce682d01fc5e2dde6598f7313027343bd14f4049b82ad0402e52cd", - "sha256:762f3771ae40e111d78d77cbe9c1035e886ac04a234d3ee0856bf4ecb3749d54", - "sha256:90147dad8c22d64b2ff7331f8d4cddfdc3ee93e4879796f837bdbb2a0b141e0c", - "sha256:935cca25d35dda9e7bd46a24831dfd255307c55a07ff38fd1a92119cffc34857", - "sha256:93fbee08c48e63d5d1b39ab56fd3fdd02e6c2431c3da0f4edaf54954744c718f", - "sha256:9541c69c62d7446539f2c1c06d7046aef822940d248fa4b8962ff0302862cc1f", - "sha256:c23f03cfd7d9826cdcbad7850de67e18b4654179e01fe9bc623d37c2638eb4ef", - "sha256:c3d1f5a1d403a8e640fa0887e9f7087331abb3f33b0f2207d2cc7f213e4a864c", - "sha256:d1998e545081da0ab276bcb4b33cce85f775adb86a516e8f55b3dac87f469548", - "sha256:d5cf11bc7f0b71fb71af26af396c83dfd3f6eed56d4b6ef95d57867bf1e4ba65", - "sha256:db0480ffbfb1193ac4e1e88239f31314fe4c6cdcf9c0b8712b55414afbf80db4", - "sha256:de4ae486041878dc46e571a4c70ba337ed5233a1344c14a0790c4c4be4bbb8b4", - "sha256:de5086cd475d67113ccb6f9fae6d8fe3ac54a4f9238fd08bfdb07b03d791ff0a", - "sha256:df34312149b495d9d03492ce97471234fd9037aa5ba217c2a6ea890e9166f151", - "sha256:ead69ba488f806fe1b1b4050febafdbf206b81fa476126f3e16110c818bac396" + "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad", + "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", + "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", + "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", + "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", + "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648", + "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", + "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", + "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c", + "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", + "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", + "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", + "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", + "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", + "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", + "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", + "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", + "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", + "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", + "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", + "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", + "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", + "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", + "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", + "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", + "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", + "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", + "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842", + "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", + "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", + "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", + "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e" ], "markers": "python_version >= '3.7'", - "version": "==42.0.3" + "version": "==42.0.8" }, "daphne": { "hashes": [ @@ -406,11 +413,19 @@ }, "django": { "hashes": [ - "sha256:5dd5b787c3ba39637610fe700f54bf158e33560ea0dba600c19921e7ff926ec5", - "sha256:aaee9fb0fb4ebd4311520887ad2e33313d368846607f82a9a0ed461cd4c35b18" + "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777", + "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38" ], + "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.2.24" + "version": "==3.2.25" + }, + "django-celery-beat": { + "hashes": [ + "sha256:f75b2d129731f1214be8383e18fae6bfeacdb55dffb2116ce849222c0106f9ad" + ], + "index": "pypi", + "version": "==2.6.0" }, "django-celery-results": { "hashes": [ @@ -429,6 +444,14 @@ "markers": "python_version >= '3.6'", "version": "==3.1.5" }, + "django-timezone-field": { + "hashes": [ + "sha256:3232e7ecde66ba4464abb6f9e6b8cc739b914efb9b29dc2cf2eee451f7cc2acb", + "sha256:aa6f4965838484317b7f08d22c0d91a53d64e7bbbd34264468ae83d4023898a7" + ], + "markers": "python_version >= '3.8' and python_version < '4.0'", + "version": "==7.0" + }, "executing": { "hashes": [ "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147", @@ -561,11 +584,11 @@ }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "incremental": { "hashes": [ @@ -593,81 +616,81 @@ }, "kombu": { "hashes": [ - "sha256:0eac1bbb464afe6fb0924b21bf79460416d25d8abc52546d4f16cad94f789488", - "sha256:30e470f1a6b49c70dc6f6d13c3e4cc4e178aa6c469ceb6bcd55645385fc84b93" + "sha256:011c4cd9a355c14a1de8d35d257314a1d2456d52b7140388561acac3cf1a97bf", + "sha256:5634c511926309c7f9789f1433e9ed402616b56836ef9878f01bd59267b4c7a9" ], "markers": "python_version >= '3.8'", - "version": "==5.3.5" + "version": "==5.3.7" }, "matplotlib-inline": { "hashes": [ - "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311", - "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304" + "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", + "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca" ], - "markers": "python_version >= '3.5'", - "version": "==0.1.6" + "markers": "python_version >= '3.8'", + "version": "==0.1.7" }, "msgpack": { "hashes": [ - "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862", - "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d", - "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3", - "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", - "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0", - "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", - "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee", - "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", - "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524", - "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819", - "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc", - "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc", - "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", - "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", - "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81", - "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6", - "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d", - "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2", - "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", - "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", - "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", - "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", - "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95", - "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f", - "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", - "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", - "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", - "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61", - "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", - "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", - "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d", - "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c", - "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", - "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", - "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415", - "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", - "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d", - "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9", - "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", - "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f", - "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7", - "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681", - "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329", - "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1", - "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf", - "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c", - "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", - "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b", - "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", - "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", - "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", - "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad", - "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd", - "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7", - "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", - "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc" + "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", + "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", + "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", + "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", + "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", + "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", + "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", + "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", + "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", + "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", + "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", + "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", + "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", + "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", + "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", + "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", + "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", + "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", + "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", + "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", + "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", + "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", + "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", + "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", + "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", + "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", + "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", + "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", + "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", + "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", + "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", + "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", + "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", + "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", + "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", + "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", + "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", + "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", + "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", + "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", + "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", + "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", + "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", + "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", + "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", + "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", + "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", + "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", + "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", + "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", + "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", + "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", + "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", + "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", + "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", + "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" ], "markers": "python_version >= '3.8'", - "version": "==1.0.7" + "version": "==1.0.8" }, "oauthlib": { "hashes": [ @@ -679,11 +702,11 @@ }, "parso": { "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" + "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", + "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d" ], "markers": "python_version >= '3.6'", - "version": "==0.8.3" + "version": "==0.8.4" }, "pexpect": { "hashes": [ @@ -702,11 +725,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d", - "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6" + "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10", + "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.0.43" + "version": "==3.0.47" }, "psutil": { "hashes": [ @@ -781,35 +804,35 @@ }, "pyasn1": { "hashes": [ - "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58", - "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c" + "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", + "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==0.5.1" + "markers": "python_version >= '3.8'", + "version": "==0.6.0" }, "pyasn1-modules": { "hashes": [ - "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c", - "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d" + "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", + "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==0.3.0" + "markers": "python_version >= '3.8'", + "version": "==0.4.0" }, "pycparser": { "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.21" + "markers": "python_version >= '3.8'", + "version": "==2.22" }, "pygments": { "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" + "markers": "python_version >= '3.8'", + "version": "==2.18.0" }, "pyjwt": { "hashes": [ @@ -821,12 +844,27 @@ }, "pyopenssl": { "hashes": [ - "sha256:6aa33039a93fffa4563e655b61d11364d01264be8ccb49906101e02a334530bf", - "sha256:ba07553fb6fd6a7a2259adb9b84e12302a9a8a75c44046e8bb5d3e5ee887e3c3" + "sha256:17ed5be5936449c5418d1cd269a1a9e9081bc54c17aed272b45856a3d3dc86ad", + "sha256:cabed4bfaa5df9f1a16c0ef64a0cb65318b5cd077a7eda7d6970131ca2f41a6f" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==24.0.0" + "version": "==24.1.0" + }, + "python-crontab": { + "hashes": [ + "sha256:40067d1dd39ade3460b2ad8557c7651514cd3851deffff61c5c60e1227c5c36b", + "sha256:82cb9b6a312d41ff66fd3caf3eed7115c28c195bfb50711bc2b4b9592feb9fe5" + ], + "version": "==3.2.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" }, "python3-openid": { "hashes": [ @@ -854,42 +892,44 @@ }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.3" }, "requests-oauthlib": { "hashes": [ - "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", - "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" + "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", + "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.1" + "markers": "python_version >= '3.4'", + "version": "==2.0.0" }, "sentry-sdk": { "hashes": [ - "sha256:657abae98b0050a0316f0873d7149f951574ae6212f71d2e3a1c4c88f62d6456", - "sha256:ac5cf56bb897ec47135d239ddeedf7c1c12d406fb031a4c0caa07399ed014d7e" + "sha256:545fcc6e36c335faa6d6cda84669b6e17025f31efbf3b2211ec14efe008b75d1", + "sha256:87b3d413c87d8e7f816cc9334bff255a83d8b577db2b22042651c30c19c09190" ], "index": "pypi", - "version": "==1.40.4" + "markers": "python_version >= '3.6'", + "version": "==2.10.0" }, "service-identity": { "hashes": [ "sha256:001c0707759cb3de7e49c078a7c0c9cd12594161d3bf06b9c254fdcb1a60dc36", "sha256:0858a54aabc5b459d1aafa8a518ed2081a285087f349fe3e55197989232e2e2d" ], + "index": "pypi", "version": "==18.1.0" }, "setuptools": { "hashes": [ - "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401", - "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6" + "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5", + "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc" ], "markers": "python_version >= '3.8'", - "version": "==69.1.0" + "version": "==70.3.0" }, "six": { "hashes": [ @@ -910,11 +950,11 @@ }, "social-auth-core": { "hashes": [ - "sha256:8d16e66eb97bb7be43a023d6efa16628cdc94cefd8d8053930c98a0f676867e7", - "sha256:9d9b51b7ce2ccd0b7139e6b7f52a32cb922726de819fb13babe35f12ae89852a" + "sha256:33cf970a623c442376f9d4a86fb187579e4438649daa5b5be993d05e74d7b2db", + "sha256:d3dbeb0999ffd0e68aa4bd73f2ac698a18133fd11b3fc890e1366f18c8889fac" ], "markers": "python_version >= '3.8'", - "version": "==4.5.3" + "version": "==4.5.4" }, "sqlparse": { "hashes": [ @@ -934,22 +974,22 @@ }, "traitlets": { "hashes": [ - "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74", - "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e" + "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", + "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f" ], "markers": "python_version >= '3.8'", - "version": "==5.14.1" + "version": "==5.14.3" }, "twisted": { "extras": [ "tls" ], "hashes": [ - "sha256:4ae8bce12999a35f7fe6443e7f1893e6fe09588c8d2bed9c35cdce8ff2d5b444", - "sha256:987847a0790a2c597197613686e2784fd54167df3a55d0fb17c8412305d76ce5" + "sha256:039f2e6a49ab5108abd94de187fa92377abe5985c7a72d68d0ad266ba19eae63", + "sha256:6b38b6ece7296b5e122c9eb17da2eeab3d98a198f50ca9efd00fb03e5b4fd4ae" ], "markers": "python_full_version >= '3.8.0'", - "version": "==23.10.0" + "version": "==24.3.0" }, "txaio": { "hashes": [ @@ -961,26 +1001,27 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version < '3.10'", - "version": "==4.9.0" + "version": "==4.12.2" }, "tzdata": { "hashes": [ "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" ], + "markers": "python_version >= '2'", "version": "==2024.1" }, "urllib3": { "hashes": [ - "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", - "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], - "markers": "python_version >= '3.6'", - "version": "==2.2.1" + "markers": "python_version >= '3.8'", + "version": "==2.2.2" }, "vine": { "hashes": [ @@ -999,55 +1040,55 @@ }, "zope-interface": { "hashes": [ - "sha256:02adbab560683c4eca3789cc0ac487dcc5f5a81cc48695ec247f00803cafe2fe", - "sha256:14e02a6fc1772b458ebb6be1c276528b362041217b9ca37e52ecea2cbdce9fac", - "sha256:25e0af9663eeac6b61b231b43c52293c2cb7f0c232d914bdcbfd3e3bd5c182ad", - "sha256:2606955a06c6852a6cff4abeca38346ed01e83f11e960caa9a821b3626a4467b", - "sha256:396f5c94654301819a7f3a702c5830f0ea7468d7b154d124ceac823e2419d000", - "sha256:3b240883fb43160574f8f738e6d09ddbdbf8fa3e8cea051603d9edfd947d9328", - "sha256:3b6c62813c63c543a06394a636978b22dffa8c5410affc9331ce6cdb5bfa8565", - "sha256:4ae9793f114cee5c464cc0b821ae4d36e1eba961542c6086f391a61aee167b6f", - "sha256:4bce517b85f5debe07b186fc7102b332676760f2e0c92b7185dd49c138734b70", - "sha256:4d45d2ba8195850e3e829f1f0016066a122bfa362cc9dc212527fc3d51369037", - "sha256:4dd374927c00764fcd6fe1046bea243ebdf403fba97a937493ae4be2c8912c2b", - "sha256:506f5410b36e5ba494136d9fa04c548eaf1a0d9c442b0b0e7a0944db7620e0ab", - "sha256:59f7374769b326a217d0b2366f1c176a45a4ff21e8f7cebb3b4a3537077eff85", - "sha256:5ee9789a20b0081dc469f65ff6c5007e67a940d5541419ca03ef20c6213dd099", - "sha256:6fc711acc4a1c702ca931fdbf7bf7c86f2a27d564c85c4964772dadf0e3c52f5", - "sha256:75d2ec3d9b401df759b87bc9e19d1b24db73083147089b43ae748aefa63067ef", - "sha256:76e0531d86523be7a46e15d379b0e975a9db84316617c0efe4af8338dc45b80c", - "sha256:8af82afc5998e1f307d5e72712526dba07403c73a9e287d906a8aa2b1f2e33dd", - "sha256:8f5d2c39f3283e461de3655e03faf10e4742bb87387113f787a7724f32db1e48", - "sha256:97785604824981ec8c81850dd25c8071d5ce04717a34296eeac771231fbdd5cd", - "sha256:a3046e8ab29b590d723821d0785598e0b2e32b636a0272a38409be43e3ae0550", - "sha256:abb0b3f2cb606981c7432f690db23506b1db5899620ad274e29dbbbdd740e797", - "sha256:ac7c2046d907e3b4e2605a130d162b1b783c170292a11216479bb1deb7cadebe", - "sha256:af27b3fe5b6bf9cd01b8e1c5ddea0a0d0a1b8c37dc1c7452f1e90bf817539c6d", - "sha256:b386b8b9d2b6a5e1e4eadd4e62335571244cb9193b7328c2b6e38b64cfda4f0e", - "sha256:b66335bbdbb4c004c25ae01cc4a54fd199afbc1fd164233813c6d3c2293bb7e1", - "sha256:d54f66c511ea01b9ef1d1a57420a93fbb9d48a08ec239f7d9c581092033156d0", - "sha256:de125151a53ecdb39df3cb3deb9951ed834dd6a110a9e795d985b10bb6db4532", - "sha256:de7916380abaef4bb4891740879b1afcba2045aee51799dfd6d6ca9bdc71f35f", - "sha256:e2fefad268ff5c5b314794e27e359e48aeb9c8bb2cbb5748a071757a56f6bb8f", - "sha256:e7b2bed4eea047a949296e618552d3fed00632dc1b795ee430289bdd0e3717f3", - "sha256:e87698e2fea5ca2f0a99dff0a64ce8110ea857b640de536c76d92aaa2a91ff3a", - "sha256:ede888382882f07b9e4cd942255921ffd9f2901684198b88e247c7eabd27a000", - "sha256:f444de0565db46d26c9fa931ca14f497900a295bd5eba480fc3fad25af8c763e", - "sha256:fa994e8937e8ccc7e87395b7b35092818905cf27c651e3ff3e7f29729f5ce3ce", - "sha256:febceb04ee7dd2aef08c2ff3d6f8a07de3052fc90137c507b0ede3ea80c21440" + "sha256:00b5c3e9744dcdc9e84c24ed6646d5cf0cf66551347b310b3ffd70f056535854", + "sha256:0e4fa5d34d7973e6b0efa46fe4405090f3b406f64b6290facbb19dcbf642ad6b", + "sha256:136cacdde1a2c5e5bc3d0b2a1beed733f97e2dad8c2ad3c2e17116f6590a3827", + "sha256:1730c93a38b5a18d24549bc81613223962a19d457cfda9bdc66e542f475a36f4", + "sha256:1a62fd6cd518693568e23e02f41816adedfca637f26716837681c90b36af3671", + "sha256:1c207e6f6dfd5749a26f5a5fd966602d6b824ec00d2df84a7e9a924e8933654e", + "sha256:2eccd5bef45883802848f821d940367c1d0ad588de71e5cabe3813175444202c", + "sha256:33ee982237cffaf946db365c3a6ebaa37855d8e3ca5800f6f48890209c1cfefc", + "sha256:3d136e5b8821073e1a09dde3eb076ea9988e7010c54ffe4d39701adf0c303438", + "sha256:47654177e675bafdf4e4738ce58cdc5c6d6ee2157ac0a78a3fa460942b9d64a8", + "sha256:47937cf2e7ed4e0e37f7851c76edeb8543ec9b0eae149b36ecd26176ff1ca874", + "sha256:4ac46298e0143d91e4644a27a769d1388d5d89e82ee0cf37bf2b0b001b9712a4", + "sha256:4c0b208a5d6c81434bdfa0f06d9b667e5de15af84d8cae5723c3a33ba6611b82", + "sha256:551db2fe892fcbefb38f6f81ffa62de11090c8119fd4e66a60f3adff70751ec7", + "sha256:599f3b07bde2627e163ce484d5497a54a0a8437779362395c6b25e68c6590ede", + "sha256:5ef8356f16b1a83609f7a992a6e33d792bb5eff2370712c9eaae0d02e1924341", + "sha256:5fe919027f29b12f7a2562ba0daf3e045cb388f844e022552a5674fcdf5d21f1", + "sha256:6f0a6be264afb094975b5ef55c911379d6989caa87c4e558814ec4f5125cfa2e", + "sha256:706efc19f9679a1b425d6fa2b4bc770d976d0984335eaea0869bd32f627591d2", + "sha256:73f9752cf3596771c7726f7eea5b9e634ad47c6d863043589a1c3bb31325c7eb", + "sha256:762e616199f6319bb98e7f4f27d254c84c5fb1c25c908c2a9d0f92b92fb27530", + "sha256:866a0f583be79f0def667a5d2c60b7b4cc68f0c0a470f227e1122691b443c934", + "sha256:86a94af4a88110ed4bb8961f5ac72edf782958e665d5bfceaab6bf388420a78b", + "sha256:8e0343a6e06d94f6b6ac52fbc75269b41dd3c57066541a6c76517f69fe67cb43", + "sha256:97e615eab34bd8477c3f34197a17ce08c648d38467489359cb9eb7394f1083f7", + "sha256:a96e6d4074db29b152222c34d7eec2e2db2f92638d2b2b2c704f9e8db3ae0edc", + "sha256:b912750b13d76af8aac45ddf4679535def304b2a48a07989ec736508d0bbfbde", + "sha256:bc2676312cc3468a25aac001ec727168994ea3b69b48914944a44c6a0b251e79", + "sha256:cebff2fe5dc82cb22122e4e1225e00a4a506b1a16fafa911142ee124febf2c9e", + "sha256:d22fce0b0f5715cdac082e35a9e735a1752dc8585f005d045abb1a7c20e197f9", + "sha256:d3f7e001328bd6466b3414215f66dde3c7c13d8025a9c160a75d7b2687090d15", + "sha256:d3fe667935e9562407c2511570dca14604a654988a13d8725667e95161d92e9b", + "sha256:dabb70a6e3d9c22df50e08dc55b14ca2a99da95a2d941954255ac76fd6982bc5", + "sha256:e2fb8e8158306567a3a9a41670c1ff99d0567d7fc96fa93b7abf8b519a46b250", + "sha256:e96ac6b3169940a8cd57b4f2b8edcad8f5213b60efcd197d59fbe52f0accd66e", + "sha256:fbf649bc77510ef2521cf797700b96167bb77838c40780da7ea3edd8b78044d1" ], "markers": "python_version >= '3.7'", - "version": "==6.2" + "version": "==6.4.post2" } }, "develop": { "asgiref": { "hashes": [ - "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e", - "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed" + "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", + "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" ], - "markers": "python_version >= '3.7'", - "version": "==3.7.2" + "markers": "python_version >= '3.8'", + "version": "==3.8.1" }, "astroid": { "hashes": [ @@ -1164,11 +1205,12 @@ }, "django": { "hashes": [ - "sha256:5dd5b787c3ba39637610fe700f54bf158e33560ea0dba600c19921e7ff926ec5", - "sha256:aaee9fb0fb4ebd4311520887ad2e33313d368846607f82a9a0ed461cd4c35b18" + "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777", + "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38" ], + "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==3.2.24" + "version": "==3.2.25" }, "django-stubs": { "hashes": [ @@ -1302,11 +1344,11 @@ }, "platformdirs": { "hashes": [ - "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", - "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", + "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" ], "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.2.2" }, "pycodestyle": { "hashes": [ @@ -1372,16 +1414,17 @@ "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], + "index": "pypi", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==5.4.1" }, "setuptools": { "hashes": [ - "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401", - "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6" + "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5", + "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc" ], "markers": "python_version >= '3.8'", - "version": "==69.1.0" + "version": "==70.3.0" }, "sqlparse": { "hashes": [ @@ -1446,11 +1489,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version < '3.10'", - "version": "==4.9.0" + "version": "==4.12.2" }, "wrapt": { "hashes": [ diff --git a/celerybeat-schedule.bak b/celerybeat-schedule.bak new file mode 100644 index 00000000..5fad0d12 --- /dev/null +++ b/celerybeat-schedule.bak @@ -0,0 +1,4 @@ +'entries', (0, 417) +'__version__', (512, 15) +'tz', (1024, 26) +'utc_enabled', (1536, 4) diff --git a/celerybeat-schedule.dat b/celerybeat-schedule.dat new file mode 100644 index 00000000..0a0bf178 Binary files /dev/null and b/celerybeat-schedule.dat differ diff --git a/celerybeat-schedule.dir b/celerybeat-schedule.dir new file mode 100644 index 00000000..5fad0d12 --- /dev/null +++ b/celerybeat-schedule.dir @@ -0,0 +1,4 @@ +'entries', (0, 417) +'__version__', (512, 15) +'tz', (1024, 26) +'utc_enabled', (1536, 4) diff --git a/manage.py b/manage.py index a29aa178..a55c3c5b 100755 --- a/manage.py +++ b/manage.py @@ -3,6 +3,8 @@ import os import sys +import sys +print(sys.version) def main(): os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'othello.settings') diff --git a/othello/apps/auth/migrations/0008_user_is_gauntlet_running.py b/othello/apps/auth/migrations/0008_user_is_gauntlet_running.py new file mode 100644 index 00000000..25f04008 --- /dev/null +++ b/othello/apps/auth/migrations/0008_user_is_gauntlet_running.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-05-25 02:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0007_auto_20210422_1152'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_gauntlet_running', + field=models.BooleanField(default=False), + ), + ] diff --git a/othello/apps/auth/migrations/0009_user_last_gauntlet_run.py b/othello/apps/auth/migrations/0009_user_last_gauntlet_run.py new file mode 100644 index 00000000..9d6fa98c --- /dev/null +++ b/othello/apps/auth/migrations/0009_user_last_gauntlet_run.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-05-28 03:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0008_user_is_gauntlet_running'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='last_gauntlet_run', + field=models.DateTimeField(null=True), + ), + ] diff --git a/othello/apps/auth/models.py b/othello/apps/auth/models.py index 36fbb87f..b984d8ba 100644 --- a/othello/apps/auth/models.py +++ b/othello/apps/auth/models.py @@ -9,6 +9,10 @@ class User(AbstractUser): is_student = models.BooleanField(default=True, null=False) is_imported = models.BooleanField(default=False, null=False) + # rating + is_gauntlet_running = models.BooleanField(default=False, null=False) + last_gauntlet_run = models.DateTimeField(null=True) + @property def has_management_permission(self) -> bool: return self.is_teacher or self.is_staff or self.is_superuser diff --git a/othello/apps/games/migrations/0031_auto_20240523_1555_squashed_0036_game_is_gauntlet.py b/othello/apps/games/migrations/0031_auto_20240523_1555_squashed_0036_game_is_gauntlet.py new file mode 100644 index 00000000..45eb1ab7 --- /dev/null +++ b/othello/apps/games/migrations/0031_auto_20240523_1555_squashed_0036_game_is_gauntlet.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.25 on 2024-07-17 00:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [('games', '0031_auto_20240523_1555'), ('games', '0032_submission_gauntlet'), ('games', '0033_game_is_ranked'), ('games', '0034_auto_20240525_2248'), ('games', '0035_auto_20240526_1514'), ('games', '0036_game_is_gauntlet')] + + dependencies = [ + ('games', '0030_auto_20220804_1438'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='rating', + field=models.IntegerField(default=400), + ), + migrations.AddField( + model_name='submission', + name='gauntlet', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='game', + name='is_ranked', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='game', + name='ratingDelta', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='game', + name='blackRating', + field=models.IntegerField(default=0, null=True), + ), + migrations.AddField( + model_name='game', + name='whiteRating', + field=models.IntegerField(default=0, null=True), + ), + migrations.AddField( + model_name='game', + name='is_gauntlet', + field=models.BooleanField(default=False), + ), + ] diff --git a/othello/apps/games/models.py b/othello/apps/games/models.py index d477b9c8..57c625e7 100644 --- a/othello/apps/games/models.py +++ b/othello/apps/games/models.py @@ -5,7 +5,7 @@ from django.contrib.auth import get_user_model from django.contrib.postgres.fields import ArrayField from django.db import models -from django.db.models import Q +from django.db.models import Q, Subquery from django.utils import timezone from ...moderator.constants import Player @@ -28,6 +28,17 @@ def latest(self, **kwargs: Any) -> "models.query.QuerySet[Submission]": """ return self.filter(**kwargs).order_by("user", "-created_at").distinct("user") + def rated(self, **kwargs: Any) -> "models.query.QuerySet[Submission]": + """ + Returns a set of all the rated submissions for all users + """ + # return self.filter(**kwargs).order_by("user", "-created_at").distinct("user").order_by("-rating") + g1 = self.latest() # Submission.objects.filter(user__in=Subquery(self.latest().values('user'))) + g2 = self.filter(gauntlet=True) + g3 = g2.intersection(g1) + g3 = g3.order_by("-rating") + return g3 + class Submission(models.Model): @@ -38,6 +49,10 @@ class Submission(models.Model): created_at = models.DateTimeField(auto_now=True) code = models.FileField(upload_to=_save_path, default=None) + # Rating Info + rating = models.IntegerField(default=400, null=False) + gauntlet = models.BooleanField(default=False, null=False) + is_legacy = models.BooleanField(default=False) tournament_win_year = models.IntegerField(default=-1) @@ -102,8 +117,15 @@ class Game(models.Model): forfeit = models.BooleanField(default=False) outcome = models.CharField(max_length=1, choices=OUTCOME_CHOICES, default="T") score = models.IntegerField(default=0) + ratingDelta = models.IntegerField(default=0) + + # rating values for both sides before rating delta is applied + blackRating = models.IntegerField(null=True, default=0) + whiteRating = models.IntegerField(null=True, default=0) is_tournament = models.BooleanField(default=False) + is_ranked = models.BooleanField(default=False) + is_gauntlet = models.BooleanField(default=False) playing = models.BooleanField(default=False) last_heartbeat = models.DateTimeField(default=timezone.now) diff --git a/othello/apps/games/tasks.py b/othello/apps/games/tasks.py index 5bd899c0..8533ef9f 100644 --- a/othello/apps/games/tasks.py +++ b/othello/apps/games/tasks.py @@ -26,14 +26,14 @@ def send_through_game_channel(game: Game, event_type: str, object_id: int) -> in def check_heartbeat(game: Game) -> bool: - if game.is_tournament: + if game.is_tournament or game.is_ranked or game.is_gauntlet: return True game.refresh_from_db() return (timezone.now() - game.last_heartbeat).seconds < settings.CLIENT_HEARTBEAT_INTERVAL * 2 def delete_game(game: Game) -> None: - if not game.is_tournament: + if not game.is_tournament and not game.is_ranked and not game.is_gauntlet: game.delete() @@ -80,93 +80,102 @@ def run_game(game_id: int) -> Optional[str]: send_through_game_channel(game, "game.error", file_deleted.id) raise RuntimeError("Cannot find a submission code file!") - error = 0 - with black_runner as player_black, white_runner as player_white: - last_move = game.moves.create(board=INITIAL_BOARD, player="-", possible=[26, 19, 44, 37]) - send_through_game_channel(game, "game.update", game_id) - exception = None - - while not mod.is_game_over(): - if not check_heartbeat(game) or not game.playing: - game.playing = False - game.outcome = "T" - game.forfeit = False - game.save(update_fields=["playing", "outcome", "forfeit"]) - return "no ping" - board, current_player = mod.get_game_state() - - try: - if current_player == Player.BLACK: - running_turn = player_black.get_move(board, current_player, black_time_limit, last_move) - elif current_player == Player.WHITE: - running_turn = player_white.get_move(board, current_player, white_time_limit, last_move) - except BaseException as e: - logger.error(f"Error when getting move {game_id}, {current_player}, {str(e)}") - task_logger.error(str(e)) - exception = e - - for log in running_turn: - print(log) - game_log = game.logs.create(player=current_player.value, message=log) - send_through_game_channel(game, "game.log", game_log.id) - submitted_move, error, extra_time = running_turn.return_value - - if exception is not None: - error = ServerError.UNEXPECTED - - if game.runoff: - if current_player == Player.BLACK: - black_time_limit = game.time_limit + extra_time - else: - white_time_limit = game.time_limit + extra_time - - if error != 0: - game_err = game.errors.create(player=current_player.value, error_code=error.value[0], error_msg=error.value[1]) - if isinstance(error, ServerError): - game.forfeit = False - game.outcome = "T" - elif isinstance(error, UserError): - game.forfeit = True - game.outcome = Player.BLACK.value if current_player == Player.WHITE else Player.WHITE.value - game.playing = False - game.save(update_fields=["forfeit", "outcome", "playing"]) - send_through_game_channel(game, "game.error", game_err.id) - break - - try: - if submitted := mod.submit_move(submitted_move): - possible = submitted - else: - game_over = True - except InvalidMoveError as e: - game_err = game.errors.create(player=current_player.value, error_code=e.code, error_msg=e.message) - game.forfeit, game.playing = True, False - game.outcome = current_player.opposite_player().value - game.save(update_fields=["forfeit", "outcome", "playing"]) - send_through_game_channel(game, "game.error", game_err.id) - task_logger.info(f"{game_id}: {current_player.value} submitted invalid move {submitted}") - break - - last_move = game.moves.create( - player=current_player.value, - move=submitted_move, - board=mod.get_board(), - possible=possible, - ) - if game_over: - game.forfeit = False - game.outcome = mod.outcome() - game.score = mod.score() - game.save(update_fields=["forfeit", "score", "outcome", "playing"]) - task_logger.info(f"GAME {game_id} OVER") - break + # print("IM HERE NOW") + + try: + error = 0 + with black_runner as player_black, white_runner as player_white: + last_move = game.moves.create(board=INITIAL_BOARD, player="-", possible=[26, 19, 44, 37]) send_through_game_channel(game, "game.update", game_id) + exception = None + + while not mod.is_game_over(): + if not check_heartbeat(game) or not game.playing: + game.playing = False + game.outcome = "T" + game.forfeit = False + game.save(update_fields=["playing", "outcome", "forfeit"]) + return "no ping" + board, current_player = mod.get_game_state() + # print(board) + + try: + if current_player == Player.BLACK: + running_turn = player_black.get_move(board, current_player, black_time_limit, last_move) + elif current_player == Player.WHITE: + running_turn = player_white.get_move(board, current_player, white_time_limit, last_move) + except BaseException as e: + logger.error(f"Error when getting move {game_id}, {current_player}, {str(e)}") + task_logger.error(str(e)) + exception = e + + for log in running_turn: + print(log) + game_log = game.logs.create(player=current_player.value, message=log) + send_through_game_channel(game, "game.log", game_log.id) + submitted_move, error, extra_time = running_turn.return_value + + if exception is not None: + error = ServerError.UNEXPECTED + + if game.runoff: + if current_player == Player.BLACK: + black_time_limit = game.time_limit + extra_time + else: + white_time_limit = game.time_limit + extra_time + + if error != 0: + game_err = game.errors.create(player=current_player.value, error_code=error.value[0], error_msg=error.value[1]) + if isinstance(error, ServerError): + game.forfeit = False + game.outcome = "T" + elif isinstance(error, UserError): + game.forfeit = True + game.outcome = Player.BLACK.value if current_player == Player.WHITE else Player.WHITE.value + game.playing = False + game.save(update_fields=["forfeit", "outcome", "playing"]) + send_through_game_channel(game, "game.error", game_err.id) + break + + try: + if submitted := mod.submit_move(submitted_move): + possible = submitted + else: + game_over = True + except InvalidMoveError as e: + game_err = game.errors.create(player=current_player.value, error_code=e.code, error_msg=e.message) + game.forfeit, game.playing = True, False + game.outcome = current_player.opposite_player().value + game.save(update_fields=["forfeit", "outcome", "playing"]) + send_through_game_channel(game, "game.error", game_err.id) + task_logger.info(f"{game_id}: {current_player.value} submitted invalid move {submitted}") + break + + last_move = game.moves.create( + player=current_player.value, + move=submitted_move, + board=mod.get_board(), + possible=possible, + ) + if game_over: + game.forfeit = False + game.outcome = mod.outcome() + game.score = mod.score() + game.save(update_fields=["forfeit", "score", "outcome", "playing"]) + task_logger.info(f"GAME {game_id} OVER") + break + send_through_game_channel(game, "game.update", game_id) + except BaseException as error: + print(error) + game.playing = False game.save(update_fields=["playing"]) send_through_game_channel(game, "game.update", game_id) black_runner.stop() white_runner.stop() + # print("DONE", game.playing, game.score) + if error != 0 and isinstance(error, ServerError): if error.value[0] != -8: raise RuntimeError(f"Game {game_id} encountered a ServerError of value {error.value}") diff --git a/othello/apps/rating/__init__.py b/othello/apps/rating/__init__.py new file mode 100644 index 00000000..f1d118d2 --- /dev/null +++ b/othello/apps/rating/__init__.py @@ -0,0 +1,2 @@ +# things that i messed with +# import_strategy_sandboxed, __init__.py in sandboxing diff --git a/othello/apps/rating/admin.py b/othello/apps/rating/admin.py new file mode 100644 index 00000000..f391f8d3 --- /dev/null +++ b/othello/apps/rating/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from .models import Gauntlet, RankedManager + + +class RankedManagerAdmin(admin.ModelAdmin): + readonly_fields = ("id",) + + +class GauntletAdmin(admin.ModelAdmin): + readonly_fields = ("id",) + + +admin.site.register(RankedManager, RankedManagerAdmin) +admin.site.register(Gauntlet, GauntletAdmin) diff --git a/othello/apps/rating/apps.py b/othello/apps/rating/apps.py new file mode 100644 index 00000000..b8bb6253 --- /dev/null +++ b/othello/apps/rating/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RatingConfig(AppConfig): + name = "othello.apps.rating" + label = "rating" diff --git a/othello/apps/rating/forms.py b/othello/apps/rating/forms.py new file mode 100644 index 00000000..73524ab1 --- /dev/null +++ b/othello/apps/rating/forms.py @@ -0,0 +1,13 @@ +from django import forms + + +class MultipleChoiceForm(forms.Form): + CHOICES = [ + ("deletegames", "Delete all ranked games models"), + ("deletegauntlets", "Delete all gauntlet models"), + ("disableauto", "Disable auto ranked games (also terminates if current)"), + ("enableauto", "Enable auto ranked games"), + ("initranked", "Initialize ranked (when the server starts for the first time)"), + ] + + choices = forms.ChoiceField(choices=CHOICES, widget=forms.RadioSelect) diff --git a/othello/apps/rating/migrations/0001_initial_squashed_0025_auto_20240716_1934.py b/othello/apps/rating/migrations/0001_initial_squashed_0025_auto_20240716_1934.py new file mode 100644 index 00000000..274aefc5 --- /dev/null +++ b/othello/apps/rating/migrations/0001_initial_squashed_0025_auto_20240716_1934.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2.25 on 2024-07-17 00:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import othello.apps.games.validators + + +class Migration(migrations.Migration): + + replaces = [('rating', '0001_initial'), ('rating', '0002_auto_20240525_1335'), ('rating', '0003_auto_20240525_2011'), ('rating', '0004_alter_gauntlet_myside1'), ('rating', '0005_auto_20240526_1510'), ('rating', '0006_alter_gauntlet_myside2'), ('rating', '0007_auto_20240526_1512'), ('rating', '0008_auto_20240526_1514'), ('rating', '0009_auto_20240526_1536'), ('rating', '0010_alter_gauntlet_myside3'), ('rating', '0011_auto_20240527_2119'), ('rating', '0012_gauntlet_pastrating'), ('rating', '0013_gauntlet_celery_task_id'), ('rating', '0014_alter_gauntlet_myside2'), ('rating', '0015_auto_20240528_0833'), ('rating', '0016_alter_gauntlet_myside2'), ('rating', '0017_auto_20240716_1826'), ('rating', '0018_auto_20240716_1826'), ('rating', '0019_auto_20240716_1828'), ('rating', '0020_auto_20240716_1829'), ('rating', '0021_auto_20240716_1830'), ('rating', '0022_auto_20240716_1831'), ('rating', '0023_auto_20240716_1932'), ('rating', '0024_auto_20240716_1934'), ('rating', '0025_auto_20240716_1934')] + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('games', '0032_submission_gauntlet'), + ('games', '0036_game_is_gauntlet'), + ('django_celery_beat', '0018_improve_crontab_helptext'), + ] + + operations = [ + migrations.CreateModel( + name='RankedManager', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('auto_run', models.BooleanField(default=False)), + ('next_auto_run', models.DateTimeField()), + ('running', models.BooleanField(default=False)), + ('game', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='g', to='games.game')), + ], + ), + migrations.CreateModel( + name='Gauntlet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now=True)), + ('game_time_limit', models.IntegerField(default=5, validators=[othello.apps.games.validators.validate_game_time_limit])), + ('running', models.BooleanField(default=False)), + ('finished', models.BooleanField(default=False)), + ('terminated', models.BooleanField(default=False)), + ('submission', models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT, related_name='gauntletsubmission', to='games.submission')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('game1', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='g1', to='games.game')), + ('game2', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='g2', to='games.game')), + ('game3', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='g3', to='games.game')), + ('mySide1', models.CharField(default='x', max_length=1)), + ('mySide2', models.CharField(default='o', max_length=1)), + ('mySide3', models.CharField(default='o', max_length=1)), + ('pastRating', models.IntegerField(default=400)), + ('celery_task_id', models.CharField(default='', max_length=48)), + ], + ), + ] diff --git a/othello/apps/rating/migrations/__init__.py b/othello/apps/rating/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/othello/apps/rating/models.py b/othello/apps/rating/models.py new file mode 100644 index 00000000..8a61e1c2 --- /dev/null +++ b/othello/apps/rating/models.py @@ -0,0 +1,77 @@ +import random +from typing import Any + +from django.contrib.auth import get_user_model +from django.db import models +from django.utils.timezone import now + +from ..auth.models import User +from ..games.models import Game, Submission +from ..games.validators import validate_game_time_limit + +# from .validators import validate_tournament_rounds + + + + +class Gauntlet(models.Model): + # objects: Any = TournamentSet().as_manager() + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, null=True) + + created_at = models.DateTimeField(auto_now=True) + game_time_limit = models.IntegerField(default=5, validators=[validate_game_time_limit]) + + running = models.BooleanField(default=False) + finished = models.BooleanField(default=False) + terminated = models.BooleanField(default=False) + + celery_task_id = models.CharField(max_length=48, default="") + + # asdf = models.ForeignKey(Submission, on_delete=models.PROTECT, null = False) + submission = models.ForeignKey( + Submission, + blank=False, + null=False, + on_delete=models.PROTECT, + related_name="gauntletsubmission", + default=None, + ) + pastRating = models.IntegerField(default=400) + + # celery_task_id = models.CharField(max_length=48, default="") + game1 = models.ForeignKey(Game, on_delete=models.CASCADE, related_name="g1", null=False, blank=False, default=None) + game2 = models.ForeignKey(Game, on_delete=models.CASCADE, related_name="g2", null=False, blank=False, default=None) + game3 = models.ForeignKey(Game, on_delete=models.CASCADE, related_name="g3", null=False, blank=False, default=None) + + mySide1 = models.CharField(default=random.choice(["x", "o"]), max_length=1) + mySide2 = models.CharField(default=random.choice(["x", "o"]), max_length=1) + mySide3 = models.CharField(default=random.choice(["x", "o"]), max_length=1) + + def __str__(self) -> str: + return "gauntlet object" + + def __repr__(self) -> str: + return f"gauntlet object {self.user} {self.created_at}" + + +class RankedManager(models.Model): + auto_run = models.BooleanField(default=False, null=False) + next_auto_run = models.DateTimeField() + running = models.BooleanField(default=False, null=False) + game = models.ForeignKey(Game, on_delete=models.PROTECT, related_name="g", null=True, blank=True, default=None) + + def __str__(self) -> str: + return f"Auto Run {self.auto_run}, Next Auto {self.next_auto_run}, Running {self.running}" + + def __repr__(self) -> str: + return f"Auto Run {self.auto_run}, Next Auto {self.next_auto_run}, Running {self.running}" + + +# class RankedGame(models.Model): + +# batch = models.DateField() + +# game = models.ForeignKey(Game, on_delete=models.CASCADE, null=False, blank=False) + +# def __str__(self) -> str: +# return f"Ranked game @ {str(self.batch)} - {self.game.black.get_user_name()} v. {self.game.white.get_user_name()}" diff --git a/othello/apps/rating/tasks.py b/othello/apps/rating/tasks.py new file mode 100644 index 00000000..68a102d7 --- /dev/null +++ b/othello/apps/rating/tasks.py @@ -0,0 +1,267 @@ +import logging +import random +import sys +import time +from datetime import datetime, timedelta +from typing import Iterator, List, Optional, Tuple, TypeVar + +from asgiref.sync import async_to_sync +from celery import shared_task +from celery.utils.log import get_task_logger +from channels.layers import get_channel_layer + +from django.conf import settings +from django.utils import timezone + +from ..auth.models import User +from ..games.models import Game, Submission +from ..games.tasks import run_game +from .models import Gauntlet, RankedManager + +T = TypeVar("T") + +logger = logging.getLogger("othello") +task_logger = get_task_logger(__name__) + + +def chunks(v: List[T], n: int) -> Iterator[Tuple[T]]: + for i in range(0, len(v), n): + yield tuple(v[i: i + n]) + + +@shared_task +def runGauntlet(user_id: int) -> str: + user = User.objects.filter(id=user_id).first() + myGauntlet = Gauntlet.objects.filter(user=user, finished=False).first() + + # with transaction.atomic(): + myGauntlet.running = True # this is redundancy + myGauntlet.save() + + myGauntlet.game1.playing = True + myGauntlet.game2.playing = True + myGauntlet.game3.playing = True + myGauntlet.game3.save() + myGauntlet.game2.save() + myGauntlet.game1.save() + + run_game(myGauntlet.game1.id) + # myGauntlet.game1.refresh_from_db() + # print("game 1 is finished") + # print(myGauntlet.game1.outcome, myGauntlet.game1.playing, myGauntlet.game1.score) + + run_game(myGauntlet.game2.id) + # myGauntlet.game2.refresh_from_db() + # print("game 2 is finished") + # print(myGauntlet.game2.outcome, myGauntlet.game2.playing, myGauntlet.game2.score) + + run_game(myGauntlet.game3.id) + # myGauntlet.game3.refresh_from_db() + # print("game 3 is finished") + # print(myGauntlet.game3.outcome, myGauntlet.game3.playing, myGauntlet.game3.score) + + user.is_gauntlet_running = False + user.save() # update_fields=["is_gauntlet_running"] + + myGauntlet.finished = True + myGauntlet.save() + + myGauntlet.refresh_from_db() + if myGauntlet.game1.outcome == myGauntlet.mySide1 and myGauntlet.game2.outcome == myGauntlet.mySide2 and myGauntlet.game3.outcome == myGauntlet.mySide3: + myGauntlet.submission.gauntlet = True + myGauntlet.submission.rating = myGauntlet.pastRating + myGauntlet.submission.save() + + allGauntlets = Gauntlet.objects.filter(finished=False).order_by("-created_at") + if allGauntlets.first(): + allGauntlets.first().running = True + allGauntlets.first().save() + runGauntlet.delay(allGauntlets.first().user.id) + + +def calculateElo(r1, r2, score): + # Let K = 32 + # WHERE r1 >= r2 + assert r1 >= r2 + p = 1 / (1 + 10 ** ((r1 - r2) / 400)) + expected = 64 * (1 - p) - 32 + + score = score * p + (32 if score > 0 else -32 if score < 0 else 0) + + delta = (32 / 64) * (score - expected) + return round(delta) + + +def doPairing(n): + assert n >= 2 + if n == 3: + psbl = [ + [(0, 1), (1, 2), (0, 2)], + [(1, 0), (1, 2), (0, 2)], + [(0, 1), (2, 1), (0, 2)], + [(1, 0), (2, 1), (0, 2)], + [(0, 1), (1, 2), (2, 0)], + [(1, 0), (1, 2), (2, 0)], + [(0, 1), (2, 1), (2, 0)], + [(1, 0), (2, 1), (2, 0)], + ] + return random.choice(psbl) + elif n == 2: + psbl = [ + [(0, 1), (1, 0)], + [(1, 0), (0, 1)], + ] + return random.choice(psbl) + + pairs = [] + lst = [i for i in range(n)] + + maxDelta = max(2, min(n // 10, 8)) # isn't a hard max, but is conducive + + while lst: + l = lst[0] + d = random.randint(1, min(maxDelta, len(lst) - 1)) + pairs.append((l, lst[d])) + lst.remove(lst[d]) + lst.remove(l) + + if len(lst) < 2: + break + + r = lst[-1] + d = random.randint(1, min(maxDelta, len(lst) - 1)) + pairs.append((r, lst[-1 - d])) + lst.remove(lst[-1 - d]) + lst.remove(r) + + if len(lst) < 2: + break + + lst2 = [i for i in range(n)] + if len(lst) == 1: + d1 = random.randint(lst[0] + 1, min(lst[0] + maxDelta, n - 1)) + d2 = random.randint(max(0, lst[0] - maxDelta), lst[0] - 1) + pairs.append((lst[0], d1)) + pairs.append((lst[0], d2)) + lst2.remove(d1) + lst2.remove(d2) + lst2.remove(lst[0]) + + lst = lst2 + while lst: + l = lst[0] + d = random.randint(1, min(maxDelta, len(lst) - 1)) + pairs.append((l, lst[d])) + lst.remove(lst[d]) + lst.remove(l) + + if len(lst) < 2: + break + + r = lst[-1] + d = random.randint(1, min(maxDelta, len(lst) - 1)) + pairs.append((r, lst[-1 - d])) + lst.remove(lst[-1 - d]) + lst.remove(r) + + if len(lst) < 2: + break + + # print(pairs) + # print(len(pairs)) + + assert len(pairs) == n + occur = [0 for _ in range(n)] + for i, j in pairs: + occur[i] += 1 + occur[j] += 1 + for i in range(n): + assert occur[i] == 2 + + return pairs + + +def deleteAllRankedGames(): + games = Game.objects.filter(is_ranked=True) + games.delete() + # games.save() + + +gameQueue = [] +lastTime = datetime.now() +# will not run the first task, to prevent a bunch of tasks built up all being run at once + + +@shared_task +def rankedSchedulerProcess(): + global lastTime + + if datetime.now() - lastTime < timedelta(seconds=45): + print("skipping") + return + + lastTime = datetime.now() + + manager = RankedManager.objects.first() + if manager.game and not manager.game.playing: + game = manager.game + # the game finished, let's update ELO + r1 = game.black.rating + r2 = game.white.rating + if r1 >= r2: + delta = calculateElo(r1, r2, game.score) + game.ratingDelta = delta + r1 += delta + r2 -= delta + else: + delta = calculateElo(r2, r1, -game.score) + game.ratingDelta = -delta + r1 -= delta + r2 += delta + game.black.rating = r1 + game.white.rating = r2 + game.black.save() + game.white.save() + game.save() + manager.running = False + elif manager.game and manager.game.playing: + return + + if manager.auto_run: + global gameQueue + if len(gameQueue) == 0: # then we have to queue the players! + players = Submission.objects.rated() + if players.count() < 2: + logger.warn("Too few players eligble for ranked!") + return + matches = doPairing(players.count()) + submissions = list(players) + for tpl in matches: + gameQueue.append((submissions[tpl[0]], submissions[tpl[1]])) + logger.warn(f"Created game queue: {str(gameQueue)}") + + if not manager.running: + black = gameQueue[-1][0] + white = gameQueue[-1][1] + logger.warn(f"Queueing ranked game --> {black} vs. {white}") + + game = Game.objects.create( + black=black, + white=white, + blackRating=black.rating, + whiteRating=white.rating, + time_limit=5, + playing=True, + last_heartbeat=timezone.now(), + runoff=False, + is_ranked=True, + ) + + manager.game = game + manager.running = True + manager.save() + + run_game.delay(manager.game.id) + gameQueue.pop() + + manager.save() diff --git a/othello/apps/rating/templatetags/__init__.py b/othello/apps/rating/templatetags/__init__.py new file mode 100644 index 00000000..46ceee97 --- /dev/null +++ b/othello/apps/rating/templatetags/__init__.py @@ -0,0 +1 @@ +# todo, make ranked games seperate from gauntlet games diff --git a/othello/apps/rating/templatetags/extras.py b/othello/apps/rating/templatetags/extras.py new file mode 100644 index 00000000..c26381a4 --- /dev/null +++ b/othello/apps/rating/templatetags/extras.py @@ -0,0 +1,31 @@ +from django import template + +register = template.Library() + + +@register.filter +def parseBlack(game): + if game.ratingDelta >= 0: + return f"{game.blackRating} + {abs(game.ratingDelta)}" + elif game.ratingDelta < 0: + return f"{game.blackRating} - {abs(game.ratingDelta)}" + return "ERROR" + + +@register.filter +def parseWhite(game): + if game.ratingDelta > 0: + return f"{game.whiteRating} - {abs(game.ratingDelta)}" + elif game.ratingDelta <= 0: + return f"{game.whiteRating} + {abs(game.ratingDelta)}" + return "ERROR" + + +@register.filter +def parseScore(game): + # a - b = score + # a + b = 64 + # 2a = 64 - score + # 2b = 64 + score + score = game.score + return f"{int(32-score/2)}-{int(32+score/2)}" diff --git a/othello/apps/rating/urls.py b/othello/apps/rating/urls.py new file mode 100644 index 00000000..66f53bfa --- /dev/null +++ b/othello/apps/rating/urls.py @@ -0,0 +1,14 @@ +from django.urls import path + +from . import views + +app_name = "rating" + +urlpatterns = [ + path("gauntlet", views.gauntlet, name="gauntlet"), + path("help", views.help, name="help"), + path("standings", views.standings, name="standings"), + path("history", views.history, name="history"), + path("manage", views.manage, name="manage"), + path("deleteGauntlet", views.deleteGauntlet, name="deleteGauntlet"), +] diff --git a/othello/apps/rating/views.py b/othello/apps/rating/views.py new file mode 100644 index 00000000..3cfe4bde --- /dev/null +++ b/othello/apps/rating/views.py @@ -0,0 +1,260 @@ +import json +import logging +import sys +from datetime import datetime, timedelta + +import pytz +from celery.result import AsyncResult +from django_celery_beat.models import IntervalSchedule, PeriodicTask + +from django.contrib.auth.decorators import login_required +from django.core.paginator import Paginator +from django.http import FileResponse, HttpRequest, HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone + +from ..auth.decorators import management_only +from ..auth.models import User +from ..games.models import Game, Submission +from .forms import MultipleChoiceForm +from .models import Gauntlet, RankedManager +from .tasks import deleteAllRankedGames, runGauntlet + +logger = logging.getLogger("othello") + + +def formatGameInfo(side, game): + if not game or not side: + return "" + if game.playing: + return "running" + payload = f"Won" if game.outcome == side else "Fail (tie or loss)" + return f"{payload}, Score: {abs(game.score) if game.outcome == side else -abs(game.score)}" + + +@login_required +def gauntlet(request: HttpRequest) -> HttpResponse: + # print(Gauntlet.objects.all()) + # request.user.is_gauntlet_running = False + # request.user.save() + # Gauntlet.objects.all().delete() + # request.user.is_superuser = True + # #request.user.is_ + # request.user.save() + # print(request.user.last_gauntlet_run, datetime.now(pytz.timezone('EST'))) + # print((datetime.now(pytz.timezone('EST')) - request.user.last_gauntlet_run).total_seconds()) + + gauntlets = Gauntlet.objects.all().filter(user=request.user) + if request.method == "GET": + if not request.user.is_gauntlet_running: # we don't have a gauntlet submitted, just render the default + choice = Submission.objects.filter(user=request.user).order_by("-created_at").first().get_submission_name() + return render( + request, + "rating/gauntlet.html", + { + "recent_submission": f"Submission: {str(choice)}", + "last_gauntlet": gauntlets.first() if gauntlets else None, + "g1": formatGameInfo(gauntlets.first().mySide1 if gauntlets else None, gauntlets.first().game1 if gauntlets else None), + "g2": formatGameInfo(gauntlets.first().mySide2 if gauntlets else None, gauntlets.first().game2 if gauntlets else None), + "g3": formatGameInfo(gauntlets.first().mySide3 if gauntlets else None, gauntlets.first().game3 if gauntlets else None), + }, + ) + else: # we have a gauntlet submitted + myGauntlet = gauntlets.filter(finished=False, user=request.user).order_by("created_at") + if myGauntlet: + myGauntlet = myGauntlet.first() + else: + return redirect("/rating/deleteGauntlet") # something weird happened, delete everything + + inQueue = True # lets check if the gauntlet is running, or just in queue + if myGauntlet.running: + inQueue = False + + return render( + request, + "rating/gauntletrunning.html", + { + "queue": inQueue, + "g1": formatGameInfo(myGauntlet.mySide1, myGauntlet.game1), + "g2": formatGameInfo(myGauntlet.mySide2, myGauntlet.game2), + "g3": formatGameInfo(myGauntlet.mySide3, myGauntlet.game3), + }, + ) + else: # post request to submit a gauntlet + # see if we have already have an unfinished gauntlet active + if request.user.last_gauntlet_run and (datetime.now(pytz.timezone("EST")) - request.user.last_gauntlet_run).total_seconds() < 60: + return render(request, "rating/throttle.html") + request.user.is_gauntlet_running = True + request.user.last_gauntlet_run = datetime.now(pytz.timezone("EST")) + request.user.save() + + myGauntlet = gauntlets.filter(finished=False).order_by("created_at") + if myGauntlet: # we have already submitted a gauntlet, stop here + return redirect("/rating/gauntlet") + else: # we create a new gauntlet + pastGauntlet = Gauntlet.objects.filter(user=request.user) + pastRating = 400 + if pastGauntlet.count() > 0: + pastGauntlet = pastGauntlet.first() + pastRating = max(400, pastGauntlet.submission.rating) + pastGauntlet.game1.delete() + pastGauntlet.game2.delete() + pastGauntlet.game3.delete() + pastGauntlet.delete() + Gauntlet.objects.filter(user=request.user).delete() + # a lot of delete redudancy + + # keep one gauntlet run saved. since we're making a new one, delete the old one + submission = Submission.objects.filter(user=request.user).order_by("-created_at").first() + + # INSERT THE CORRECT GAUNTLET BOT HERE + gauntletUser = User.objects.filter(username="warden").first() + gauntletBot = Submission.objects.filter(user=gauntletUser).first() + # ------------------------------------ + + game1 = Game.objects.create( + black=gauntletBot, + white=gauntletBot, + time_limit=5, + playing=False, + last_heartbeat=timezone.now(), + runoff=False, + is_gauntlet=True, + ) + game2 = Game.objects.create( + black=gauntletBot, + white=gauntletBot, + time_limit=5, + playing=False, + last_heartbeat=timezone.now(), + runoff=False, + is_gauntlet=True, + ) + game3 = Game.objects.create( + black=gauntletBot, + white=gauntletBot, + time_limit=5, + playing=False, + last_heartbeat=timezone.now(), + runoff=False, + is_gauntlet=True, + ) + myGauntlet = Gauntlet.objects.create(user=request.user, submission=submission, game1=game1, game2=game2, game3=game3, pastRating=pastRating) + if myGauntlet.mySide1 == "x": + myGauntlet.game1.black = submission + else: + myGauntlet.game1.white = submission + if myGauntlet.mySide2 == "x": + myGauntlet.game2.black = submission + else: + myGauntlet.game2.white = submission + if myGauntlet.mySide3 == "x": + myGauntlet.game3.black = submission + else: + myGauntlet.game3.white = submission + + myGauntlet.game1.save() + myGauntlet.game2.save() + myGauntlet.game3.save() + myGauntlet.save() + + gauntlets = Gauntlet.objects.all().filter(finished=False).order_by("created_at") + if myGauntlet == gauntlets.first() and not myGauntlet.running: # check if this gauntlet is the first one submitted, then we need to start running + myGauntlet.running = True + myGauntlet.save() + # runGauntlet(request.user.id) #.delay(request.user.id) + runGauntlet.delay(request.user.id) + return redirect("/rating/gauntlet") + + return redirect("/rating/gauntlet") # its just in queue + + +@login_required +def deleteGauntlet(request: HttpRequest) -> HttpResponse: + request.user.is_gauntlet_running = False + request.user.save() + gauntlets = Gauntlet.objects.all().filter(user=request.user).order_by("-created_at") + if gauntlets: + AsyncResult(gauntlets.first().celery_task_id).revoke(terminate=True) + gauntlets.first().game1.delete() + gauntlets.first().game2.delete() + gauntlets.first().game3.delete() + gauntlets.delete() + + return redirect("/rating/gauntlet") + + +def history(request: HttpRequest) -> HttpResponse: + if request.method == "GET": + games = list(Game.objects.filter(is_ranked=True, playing=False).order_by("-created_at")) + + return render( + request, + "rating/history.html", + {"games": games}, + ) + + return redirect("/") + + +def help(request: HttpRequest) -> HttpResponse: + return render(request, "rating/help.html") + + +def standings(request: HttpRequest) -> HttpResponse: + # Game.objects.all().delete() + players = Submission.objects.rated() + + page_obj = Paginator(players, 10).get_page(request.GET.get("page", "1")) + offset = 10 * (page_obj.number - 1) + + return render( + request, + "rating/standings.html", + {"next_time": RankedManager.objects.first().next_auto_run, "players": players, "page_obj": page_obj, "offset": offset}, + ) + + +@management_only +def manage(request: HttpRequest) -> HttpResponse: + manager = RankedManager.objects.first() + + if request.method == "POST": + form = MultipleChoiceForm(request.POST) + if form.is_valid(): + selected_choice = form.cleaned_data["choices"] + if selected_choice == "deletegames": + deleteAllRankedGames() + elif selected_choice == "deletegauntlets": + Gauntlet.objects.all().delete() + elif selected_choice == "disableauto": + manager.auto_run = False + manager.save() + elif selected_choice == "enableauto": + manager.auto_run = True + manager.save() + elif selected_choice == "initranked": + # create 1 single ranked manager object + RankedManager.objects.all().delete() + manager = RankedManager.objects.create( + auto_run=False, + running=False, + next_auto_run=timezone.now(), + ) + + # create scheduler that runs once a minute + interval, _ = IntervalSchedule.objects.get_or_create( + every=60, + period=IntervalSchedule.SECONDS, + ) + + PeriodicTask.objects.all().delete() + PeriodicTask.objects.create( + interval=interval, + name="RankedScheduler", + task="othello.apps.rating.tasks.rankedSchedulerProcess", + ) + # celery -A othello beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler + else: + form = MultipleChoiceForm() + return render(request, "rating/manage.html", {"form": form, "manager": manager}) diff --git a/othello/settings/__init__.py b/othello/settings/__init__.py index 42e6e681..982bb041 100644 --- a/othello/settings/__init__.py +++ b/othello/settings/__init__.py @@ -30,6 +30,7 @@ INSTALLED_APPS = [ "othello.apps.OthelloAdminConfig", + "django_celery_beat", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", @@ -44,6 +45,7 @@ "othello.apps.auth.apps.AuthConfig", "othello.apps.games.apps.GamesConfig", "othello.apps.tournaments.apps.TournamentsConfig", + "othello.apps.rating.apps.RatingConfig", ] MIDDLEWARE = [ diff --git a/othello/static/js/base.js b/othello/static/js/base.js index a7e19026..857c900b 100644 --- a/othello/static/js/base.js +++ b/othello/static/js/base.js @@ -24,6 +24,12 @@ tippy(".yourself", { placement: "right" }) +tippy(".score", { + content: "The score shown assumes that the game ended with 64 tokens on the board. " + + "This doesn't always happen, so the score here might be wrong.", + placement: "right" +}) + tippy("#include_users_file_help", { content: "Upload a CSV file with each included user in a separate entry, users should be split by newline." + " Users read from this file will be added to the above field.", diff --git a/othello/templates/base.html b/othello/templates/base.html index fd64f457..8771acd8 100644 --- a/othello/templates/base.html +++ b/othello/templates/base.html @@ -58,6 +58,20 @@ {% endif %} + {% if user|has_management_permissions %}