From dd1bcdb8dc235b2010f89f0c87e4695cd69589d1 Mon Sep 17 00:00:00 2001 From: Johnny Liu Date: Tue, 28 May 2024 11:19:30 -0400 Subject: [PATCH 1/6] create code for ranked games --- Pipfile | 4 +- Pipfile.lock | 619 +++++++++++------- manage.py | 2 + .../0008_user_is_gauntlet_running.py | 18 + .../migrations/0009_user_last_gauntlet_run.py | 18 + othello/apps/auth/models.py | 4 + .../migrations/0031_auto_20240523_1555.py | 23 + .../migrations/0032_submission_gauntlet.py | 18 + .../games/migrations/0033_game_is_ranked.py | 18 + .../migrations/0034_auto_20240525_2248.py | 22 + .../migrations/0035_auto_20240526_1514.py | 23 + .../games/migrations/0036_game_is_gauntlet.py | 18 + othello/apps/games/models.py | 23 +- othello/apps/games/tasks.py | 173 ++--- othello/apps/rating/__init__.py | 2 + othello/apps/rating/admin.py | 0 othello/apps/rating/apps.py | 6 + othello/apps/rating/forms.py | 12 + .../apps/rating/migrations/0001_initial.py | 32 + .../migrations/0002_auto_20240525_1335.py | 30 + .../migrations/0003_auto_20240525_2011.py | 28 + .../migrations/0004_alter_gauntlet_myside1.py | 18 + .../migrations/0005_auto_20240526_1510.py | 28 + .../migrations/0006_alter_gauntlet_myside2.py | 18 + .../migrations/0007_auto_20240526_1512.py | 23 + .../migrations/0008_auto_20240526_1514.py | 23 + .../migrations/0009_auto_20240526_1536.py | 33 + .../migrations/0010_alter_gauntlet_myside3.py | 18 + .../migrations/0011_auto_20240527_2119.py | 33 + .../migrations/0012_gauntlet_pastrating.py | 18 + .../0013_gauntlet_celery_task_id.py | 18 + .../migrations/0014_alter_gauntlet_myside2.py | 18 + .../migrations/0015_auto_20240528_0833.py | 23 + othello/apps/rating/migrations/__init__.py | 0 othello/apps/rating/models.py | 76 +++ othello/apps/rating/tasks.py | 292 +++++++++ othello/apps/rating/templatetags/__init__.py | 1 + othello/apps/rating/templatetags/extras.py | 28 + othello/apps/rating/urls.py | 14 + othello/apps/rating/views.py | 241 +++++++ othello/moderator/runners.py | 128 +++- othello/sandboxing/__init__.py | 7 +- othello/sandboxing/import_wrapper.py | 2 +- othello/settings/__init__.py | 1 + othello/settings/secret.sample.py | 2 +- othello/static/js/base.js | 6 + othello/templates/base.html | 14 + othello/templates/games/watch_list.html | 6 + othello/templates/rating/gauntlet.html | 40 ++ othello/templates/rating/gauntletrunning.html | 28 + othello/templates/rating/help.html | 45 ++ othello/templates/rating/history.html | 40 ++ othello/templates/rating/manage.html | 28 + othello/templates/rating/standings.html | 44 ++ othello/templates/rating/throttle.html | 9 + othello/urls.py | 1 + 56 files changed, 2062 insertions(+), 355 deletions(-) create mode 100644 othello/apps/auth/migrations/0008_user_is_gauntlet_running.py create mode 100644 othello/apps/auth/migrations/0009_user_last_gauntlet_run.py create mode 100644 othello/apps/games/migrations/0031_auto_20240523_1555.py create mode 100644 othello/apps/games/migrations/0032_submission_gauntlet.py create mode 100644 othello/apps/games/migrations/0033_game_is_ranked.py create mode 100644 othello/apps/games/migrations/0034_auto_20240525_2248.py create mode 100644 othello/apps/games/migrations/0035_auto_20240526_1514.py create mode 100644 othello/apps/games/migrations/0036_game_is_gauntlet.py create mode 100644 othello/apps/rating/__init__.py create mode 100644 othello/apps/rating/admin.py create mode 100644 othello/apps/rating/apps.py create mode 100644 othello/apps/rating/forms.py create mode 100644 othello/apps/rating/migrations/0001_initial.py create mode 100644 othello/apps/rating/migrations/0002_auto_20240525_1335.py create mode 100644 othello/apps/rating/migrations/0003_auto_20240525_2011.py create mode 100644 othello/apps/rating/migrations/0004_alter_gauntlet_myside1.py create mode 100644 othello/apps/rating/migrations/0005_auto_20240526_1510.py create mode 100644 othello/apps/rating/migrations/0006_alter_gauntlet_myside2.py create mode 100644 othello/apps/rating/migrations/0007_auto_20240526_1512.py create mode 100644 othello/apps/rating/migrations/0008_auto_20240526_1514.py create mode 100644 othello/apps/rating/migrations/0009_auto_20240526_1536.py create mode 100644 othello/apps/rating/migrations/0010_alter_gauntlet_myside3.py create mode 100644 othello/apps/rating/migrations/0011_auto_20240527_2119.py create mode 100644 othello/apps/rating/migrations/0012_gauntlet_pastrating.py create mode 100644 othello/apps/rating/migrations/0013_gauntlet_celery_task_id.py create mode 100644 othello/apps/rating/migrations/0014_alter_gauntlet_myside2.py create mode 100644 othello/apps/rating/migrations/0015_auto_20240528_0833.py create mode 100644 othello/apps/rating/migrations/__init__.py create mode 100644 othello/apps/rating/models.py create mode 100644 othello/apps/rating/tasks.py create mode 100644 othello/apps/rating/templatetags/__init__.py create mode 100644 othello/apps/rating/templatetags/extras.py create mode 100644 othello/apps/rating/urls.py create mode 100644 othello/apps/rating/views.py create mode 100644 othello/templates/rating/gauntlet.html create mode 100644 othello/templates/rating/gauntletrunning.html create mode 100644 othello/templates/rating/help.html create mode 100644 othello/templates/rating/history.html create mode 100644 othello/templates/rating/manage.html create mode 100644 othello/templates/rating/standings.html create mode 100644 othello/templates/rating/throttle.html diff --git a/Pipfile b/Pipfile index 162a175c1..e490cfa4d 100644 --- a/Pipfile +++ b/Pipfile @@ -4,7 +4,6 @@ verify_ssl = true name = "pypi" [packages] -celery = "~=5.2.2" channels = "~=3.0.3" channels-redis = "~=3.2.0" daphne = "~=3.0.1" @@ -20,6 +19,9 @@ service_identity = "~=18.1.0" social-auth-app-django = "~=4.0.0" sqlparse = "~=0.4.2" pyopenssl = "*" +colorama = "*" +celery = "==5.2.7" +gevent = "*" [dev-packages] flake8 = "~=3.9.2" diff --git a/Pipfile.lock b/Pipfile.lock index 458489f39..d54850eb1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "faf6dcb98cc0a6a1d4d6e3648d84b2ec8471ab6c9e258dc754027a5f0034389f" + "sha256": "7c3c92c539206eafe88f3e1102ba40fcf12eef38c3f55eeb6a339687e3ce29c3" }, "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": [ @@ -111,6 +111,7 @@ "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547", "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b" ], + "markers": "python_version >= '3.7'", "version": "==3.6.4.0" }, "celery": { @@ -185,7 +186,7 @@ "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" ], - "markers": "platform_python_implementation != 'PyPy'", + "markers": "platform_python_implementation == 'CPython' and sys_platform == 'win32'", "version": "==1.16.0" }, "channels": { @@ -312,11 +313,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": [ @@ -333,6 +334,15 @@ "markers": "python_version >= '3.6'", "version": "==0.3.0" }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" + }, "constantly": { "hashes": [ "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", @@ -343,41 +353,41 @@ }, "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:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55", + "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785", + "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b", + "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886", + "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82", + "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1", + "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda", + "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f", + "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68", + "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60", + "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7", + "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd", + "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582", + "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc", + "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858", + "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b", + "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2", + "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678", + "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13", + "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4", + "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8", + "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604", + "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477", + "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e", + "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a", + "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9", + "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14", + "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda", + "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da", + "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562", + "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2", + "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9" ], "markers": "python_version >= '3.7'", - "version": "==42.0.3" + "version": "==42.0.7" }, "daphne": { "hashes": [ @@ -406,11 +416,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-celery-results": { "hashes": [ @@ -437,6 +448,118 @@ "markers": "python_version >= '3.5'", "version": "==2.0.1" }, + "gevent": { + "hashes": [ + "sha256:03aa5879acd6b7076f6a2a307410fb1e0d288b84b03cdfd8c74db8b4bc882fc5", + "sha256:117e5837bc74a1673605fb53f8bfe22feb6e5afa411f524c835b2ddf768db0de", + "sha256:141a2b24ad14f7b9576965c0c84927fc85f824a9bb19f6ec1e61e845d87c9cd8", + "sha256:14532a67f7cb29fb055a0e9b39f16b88ed22c66b96641df8c04bdc38c26b9ea5", + "sha256:1dffb395e500613e0452b9503153f8f7ba587c67dd4a85fc7cd7aa7430cb02cc", + "sha256:2955eea9c44c842c626feebf4459c42ce168685aa99594e049d03bedf53c2800", + "sha256:2ae3a25ecce0a5b0cd0808ab716bfca180230112bb4bc89b46ae0061d62d4afe", + "sha256:2e9ac06f225b696cdedbb22f9e805e2dd87bf82e8fa5e17756f94e88a9d37cf7", + "sha256:368a277bd9278ddb0fde308e6a43f544222d76ed0c4166e0d9f6b036586819d9", + "sha256:3adfb96637f44010be8abd1b5e73b5070f851b817a0b182e601202f20fa06533", + "sha256:3d5325ccfadfd3dcf72ff88a92fb8fc0b56cacc7225f0f4b6dcf186c1a6eeabc", + "sha256:432fc76f680acf7cf188c2ee0f5d3ab73b63c1f03114c7cd8a34cebbe5aa2056", + "sha256:44098038d5e2749b0784aabb27f1fcbb3f43edebedf64d0af0d26955611be8d6", + "sha256:5a1df555431f5cd5cc189a6ee3544d24f8c52f2529134685f1e878c4972ab026", + "sha256:6c47ae7d1174617b3509f5d884935e788f325eb8f1a7efc95d295c68d83cce40", + "sha256:6f947a9abc1a129858391b3d9334c45041c08a0f23d14333d5b844b6e5c17a07", + "sha256:782a771424fe74bc7e75c228a1da671578c2ba4ddb2ca09b8f959abdf787331e", + "sha256:7899a38d0ae7e817e99adb217f586d0a4620e315e4de577444ebeeed2c5729be", + "sha256:7b00f8c9065de3ad226f7979154a7b27f3b9151c8055c162332369262fc025d8", + "sha256:8f4b8e777d39013595a7740b4463e61b1cfe5f462f1b609b28fbc1e4c4ff01e5", + "sha256:90cbac1ec05b305a1b90ede61ef73126afdeb5a804ae04480d6da12c56378df1", + "sha256:918cdf8751b24986f915d743225ad6b702f83e1106e08a63b736e3a4c6ead789", + "sha256:9202f22ef811053077d01f43cc02b4aaf4472792f9fd0f5081b0b05c926cca19", + "sha256:94138682e68ec197db42ad7442d3cf9b328069c3ad8e4e5022e6b5cd3e7ffae5", + "sha256:968581d1717bbcf170758580f5f97a2925854943c45a19be4d47299507db2eb7", + "sha256:9d8d0642c63d453179058abc4143e30718b19a85cbf58c2744c9a63f06a1d388", + "sha256:a7ceb59986456ce851160867ce4929edaffbd2f069ae25717150199f8e1548b8", + "sha256:b9913c45d1be52d7a5db0c63977eebb51f68a2d5e6fd922d1d9b5e5fd758cc98", + "sha256:bde283313daf0b34a8d1bab30325f5cb0f4e11b5869dbe5bc61f8fe09a8f66f3", + "sha256:bf5b9c72b884c6f0c4ed26ef204ee1f768b9437330422492c319470954bc4cc7", + "sha256:ca80b121bbec76d7794fcb45e65a7eca660a76cc1a104ed439cdbd7df5f0b060", + "sha256:cdf66977a976d6a3cfb006afdf825d1482f84f7b81179db33941f2fc9673bb1d", + "sha256:d4faf846ed132fd7ebfbbf4fde588a62d21faa0faa06e6f468b7faa6f436b661", + "sha256:d7f87c2c02e03d99b95cfa6f7a776409083a9e4d468912e18c7680437b29222c", + "sha256:dd23df885318391856415e20acfd51a985cba6919f0be78ed89f5db9ff3a31cb", + "sha256:f5de3c676e57177b38857f6e3cdfbe8f38d1cd754b63200c0615eaa31f514b4f", + "sha256:f5e8e8d60e18d5f7fd49983f0c4696deeddaf6e608fbab33397671e2fcc6cc91", + "sha256:f7cac622e11b4253ac4536a654fe221249065d9a69feb6cdcd4d9af3503602e0", + "sha256:f8a04cf0c5b7139bc6368b461257d4a757ea2fe89b3773e494d235b7dd51119f", + "sha256:f8bb35ce57a63c9a6896c71a285818a3922d8ca05d150fd1fe49a7f57287b836", + "sha256:fbfdce91239fe306772faab57597186710d5699213f4df099d1612da7320d682" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==24.2.1" + }, + "greenlet": { + "hashes": [ + "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", + "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", + "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", + "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", + "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", + "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", + "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", + "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", + "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", + "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", + "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", + "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", + "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", + "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", + "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", + "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", + "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", + "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", + "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", + "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", + "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", + "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", + "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", + "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", + "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", + "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", + "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", + "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", + "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", + "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", + "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", + "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", + "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", + "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", + "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", + "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", + "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", + "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", + "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", + "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", + "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", + "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", + "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", + "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", + "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", + "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", + "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", + "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", + "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", + "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", + "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", + "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", + "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", + "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", + "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", + "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", + "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", + "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" + ], + "markers": "python_version < '3.11' and platform_python_implementation == 'CPython'", + "version": "==3.0.3" + }, "hiredis": { "hashes": [ "sha256:01b6c24c0840ac7afafbc4db236fd55f56a9a0919a215c25a238f051781f4772", @@ -561,11 +684,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 +716,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,19 +802,11 @@ }, "parso": { "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" + "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", + "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d" ], "markers": "python_version >= '3.6'", - "version": "==0.8.3" - }, - "pexpect": { - "hashes": [ - "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", - "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" - ], - "markers": "sys_platform != 'win32'", - "version": "==4.9.0" + "version": "==0.8.4" }, "pickleshare": { "hashes": [ @@ -765,13 +880,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.6" }, - "ptyprocess": { - "hashes": [ - "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", - "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" - ], - "version": "==0.7.0" - }, "pure-eval": { "hashes": [ "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350", @@ -781,35 +889,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 +929,12 @@ }, "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" }, "python3-openid": { "hashes": [ @@ -854,42 +962,44 @@ }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", + "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.2" }, "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:139a71a19f5e9eb5d3623942491ce03cf8ebc14ea2e39ba3e6fe79560d8a5b1f", + "sha256:c5aeb095ba226391d337dd42a6f9470d86c9fc236ecc71cfc7cd1942b45010c6" ], "index": "pypi", - "version": "==1.40.4" + "markers": "python_version >= '3.6'", + "version": "==2.3.1" }, "service-identity": { "hashes": [ "sha256:001c0707759cb3de7e49c078a7c0c9cd12594161d3bf06b9c254fdcb1a60dc36", "sha256:0858a54aabc5b459d1aafa8a518ed2081a285087f349fe3e55197989232e2e2d" ], + "index": "pypi", "version": "==18.1.0" }, "setuptools": { "hashes": [ - "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401", - "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6" + "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", + "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0" ], "markers": "python_version >= '3.8'", - "version": "==69.1.0" + "version": "==70.0.0" }, "six": { "hashes": [ @@ -910,11 +1020,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 +1044,47 @@ }, "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" + }, + "twisted-iocpsupport": { + "hashes": [ + "sha256:0058c963c8957bcd3deda62122e89953c9de1e867a274facc9b15dde1a9f31e8", + "sha256:0c1b5cf37f0b2d96cc3c9bc86fff16613b9f5d0ca565c96cf1f1fb8cfca4b81c", + "sha256:196f7c7ccad4ba4d1783b1c4e1d1b22d93c04275cd780bf7498d16c77319ad6e", + "sha256:300437af17396a945a58dcfffd77863303a8b6d9e65c6e81f1d2eed55b50d444", + "sha256:391ac4d6002a80e15f35adc4ad6056f4fe1c17ceb0d1f98ba01b0f4f917adfd7", + "sha256:3c5dc11d72519e55f727320e3cee535feedfaee09c0f0765ed1ca7badff1ab3c", + "sha256:3d306fc4d88a6bcf61ce9d572c738b918578121bfd72891625fab314549024b5", + "sha256:4574eef1f3bb81501fb02f911298af3c02fe8179c31a33b361dd49180c3e644d", + "sha256:4e5f97bcbabdd79cbaa969b63439b89801ea560f11d42b0a387634275c633623", + "sha256:6081bd7c2f4fcf9b383dcdb3b3385d75a26a7c9d2be25b6950c3d8ea652d2d2d", + "sha256:76f7e67cec1f1d097d1f4ed7de41be3d74546e1a4ede0c7d56e775c4dce5dfb0", + "sha256:7c66fa0aa4236b27b3c61cb488662d85dae746a6d1c7b0d91cf7aae118445adf", + "sha256:858096c0d15e33f15ac157f455d8f86f2f2cdd223963e58c0f682a3af8362d89", + "sha256:872747a3b64e2909aee59c803ccd0bceb9b75bf27915520ebd32d69687040fa2", + "sha256:afa2b630797f9ed2f27f3d9f55e3f72b4244911e45a8c82756f44babbf0b243e", + "sha256:c2712b778bacf1db434e3e065adfed3db300754186a29aecac1efae9ef4bcaff", + "sha256:c27985e949b9b1a1fb4c20c71d315c10ea0f93fdf3ccdd4a8c158b5926edd8c8", + "sha256:cc86c2ef598c15d824a243c2541c29459881c67fc3c0adb6efe2242f8f0ec3af", + "sha256:e311dfcb470696e3c077249615893cada598e62fa7c4e4ca090167bd2b7d331f" + ], + "markers": "platform_system == 'Windows'", + "version": "==1.0.4" }, "txaio": { "hashes": [ @@ -961,17 +1096,18 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", + "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" ], "markers": "python_version < '3.10'", - "version": "==4.9.0" + "version": "==4.12.0" }, "tzdata": { "hashes": [ "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" ], + "markers": "python_version >= '2'", "version": "==2024.1" }, "urllib3": { @@ -979,7 +1115,7 @@ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.6'", + "markers": "python_version >= '3.8'", "version": "==2.2.1" }, "vine": { @@ -997,57 +1133,65 @@ ], "version": "==0.2.13" }, - "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" + "zope.event": { + "hashes": [ + "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", + "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" + ], + "markers": "python_version >= '3.7'", + "version": "==5.0" + }, + "zope.interface": { + "hashes": [ + "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": [ @@ -1103,6 +1247,15 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" + }, "coverage": { "hashes": [ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", @@ -1164,11 +1317,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 +1456,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 +1526,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:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", + "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0" ], "markers": "python_version >= '3.8'", - "version": "==69.1.0" + "version": "==70.0.0" }, "sqlparse": { "hashes": [ @@ -1446,11 +1601,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", + "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" ], "markers": "python_version < '3.10'", - "version": "==4.9.0" + "version": "==4.12.0" }, "wrapt": { "hashes": [ diff --git a/manage.py b/manage.py index a29aa178a..a55c3c5bc 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 000000000..25f040084 --- /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 000000000..9d6fa98cb --- /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 36fbb87f8..a7180fed1 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.py b/othello/apps/games/migrations/0031_auto_20240523_1555.py new file mode 100644 index 000000000..beed1b59f --- /dev/null +++ b/othello/apps/games/migrations/0031_auto_20240523_1555.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.24 on 2024-05-23 19:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0030_auto_20220804_1438'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='deviation', + field=models.IntegerField(default=350), + ), + migrations.AddField( + model_name='submission', + name='rating', + field=models.IntegerField(default=400), + ), + ] diff --git a/othello/apps/games/migrations/0032_submission_gauntlet.py b/othello/apps/games/migrations/0032_submission_gauntlet.py new file mode 100644 index 000000000..8bb01e75d --- /dev/null +++ b/othello/apps/games/migrations/0032_submission_gauntlet.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 = [ + ('games', '0031_auto_20240523_1555'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='gauntlet', + field=models.BooleanField(default=False), + ), + ] diff --git a/othello/apps/games/migrations/0033_game_is_ranked.py b/othello/apps/games/migrations/0033_game_is_ranked.py new file mode 100644 index 000000000..35d963d20 --- /dev/null +++ b/othello/apps/games/migrations/0033_game_is_ranked.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-05-25 17:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0032_submission_gauntlet'), + ] + + operations = [ + migrations.AddField( + model_name='game', + name='is_ranked', + field=models.BooleanField(default=False), + ), + ] diff --git a/othello/apps/games/migrations/0034_auto_20240525_2248.py b/othello/apps/games/migrations/0034_auto_20240525_2248.py new file mode 100644 index 000000000..11e1a57f1 --- /dev/null +++ b/othello/apps/games/migrations/0034_auto_20240525_2248.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.25 on 2024-05-26 02:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0033_game_is_ranked'), + ] + + operations = [ + migrations.RemoveField( + model_name='submission', + name='deviation', + ), + migrations.AddField( + model_name='game', + name='ratingDelta', + field=models.IntegerField(default=0), + ), + ] diff --git a/othello/apps/games/migrations/0035_auto_20240526_1514.py b/othello/apps/games/migrations/0035_auto_20240526_1514.py new file mode 100644 index 000000000..5cca157bf --- /dev/null +++ b/othello/apps/games/migrations/0035_auto_20240526_1514.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-05-26 19:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0034_auto_20240525_2248'), + ] + + operations = [ + 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), + ), + ] diff --git a/othello/apps/games/migrations/0036_game_is_gauntlet.py b/othello/apps/games/migrations/0036_game_is_gauntlet.py new file mode 100644 index 000000000..14d457224 --- /dev/null +++ b/othello/apps/games/migrations/0036_game_is_gauntlet.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-05-28 12:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0035_auto_20240526_1514'), + ] + + operations = [ + 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 d477b9c8a..b1ae8491e 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,16 @@ 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 +48,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 +116,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 5bd899c0d..fe1a5f8e9 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: 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: 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 000000000..8e8c3bc82 --- /dev/null +++ b/othello/apps/rating/__init__.py @@ -0,0 +1,2 @@ +# things that i messed with +# import_strategy_sandboxed, __init__.py in sandboxing \ No newline at end of file diff --git a/othello/apps/rating/admin.py b/othello/apps/rating/admin.py new file mode 100644 index 000000000..e69de29bb diff --git a/othello/apps/rating/apps.py b/othello/apps/rating/apps.py new file mode 100644 index 000000000..b8bb62532 --- /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 000000000..fd38631c5 --- /dev/null +++ b/othello/apps/rating/forms.py @@ -0,0 +1,12 @@ +from django import forms + +class MultipleChoiceForm(forms.Form): + CHOICES = [ + ('runbatch', 'Manually run ranked games batch'), + ('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'), + ] + + choices = forms.ChoiceField(choices=CHOICES, widget=forms.RadioSelect) diff --git a/othello/apps/rating/migrations/0001_initial.py b/othello/apps/rating/migrations/0001_initial.py new file mode 100644 index 000000000..c7810e2a1 --- /dev/null +++ b/othello/apps/rating/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.25 on 2024-05-25 17:01 + +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): + + initial = True + + dependencies = [ + ('games', '0032_submission_gauntlet'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + 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)), + ], + ), + ] diff --git a/othello/apps/rating/migrations/0002_auto_20240525_1335.py b/othello/apps/rating/migrations/0002_auto_20240525_1335.py new file mode 100644 index 000000000..8d2fbe5b6 --- /dev/null +++ b/othello/apps/rating/migrations/0002_auto_20240525_1335.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.25 on 2024-05-25 17:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0032_submission_gauntlet'), + ('rating', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='gauntlet', + name='game1', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='g1', to='games.game'), + ), + migrations.AddField( + model_name='gauntlet', + name='game2', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='g2', to='games.game'), + ), + migrations.AddField( + model_name='gauntlet', + name='game3', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='g3', to='games.game'), + ), + ] diff --git a/othello/apps/rating/migrations/0003_auto_20240525_2011.py b/othello/apps/rating/migrations/0003_auto_20240525_2011.py new file mode 100644 index 000000000..d09b1865d --- /dev/null +++ b/othello/apps/rating/migrations/0003_auto_20240525_2011.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.25 on 2024-05-26 00:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0002_auto_20240525_1335'), + ] + + operations = [ + migrations.AddField( + model_name='gauntlet', + name='mySide1', + field=models.CharField(default='o', max_length=1), + ), + migrations.AddField( + model_name='gauntlet', + name='mySide2', + field=models.CharField(default='o', max_length=1), + ), + migrations.AddField( + model_name='gauntlet', + name='mySide3', + field=models.CharField(default='o', max_length=1), + ), + ] diff --git a/othello/apps/rating/migrations/0004_alter_gauntlet_myside1.py b/othello/apps/rating/migrations/0004_alter_gauntlet_myside1.py new file mode 100644 index 000000000..297b86a6d --- /dev/null +++ b/othello/apps/rating/migrations/0004_alter_gauntlet_myside1.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-05-26 02:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0003_auto_20240525_2011'), + ] + + operations = [ + migrations.AlterField( + model_name='gauntlet', + name='mySide1', + field=models.CharField(default='x', max_length=1), + ), + ] diff --git a/othello/apps/rating/migrations/0005_auto_20240526_1510.py b/othello/apps/rating/migrations/0005_auto_20240526_1510.py new file mode 100644 index 000000000..fe7f98d70 --- /dev/null +++ b/othello/apps/rating/migrations/0005_auto_20240526_1510.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.25 on 2024-05-26 19:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0004_alter_gauntlet_myside1'), + ] + + operations = [ + migrations.AlterField( + model_name='gauntlet', + name='mySide1', + field=models.CharField(default='o', max_length=1), + ), + migrations.AlterField( + model_name='gauntlet', + name='mySide2', + field=models.CharField(default='x', max_length=1), + ), + migrations.AlterField( + model_name='gauntlet', + name='mySide3', + field=models.CharField(default='x', max_length=1), + ), + ] diff --git a/othello/apps/rating/migrations/0006_alter_gauntlet_myside2.py b/othello/apps/rating/migrations/0006_alter_gauntlet_myside2.py new file mode 100644 index 000000000..7a0b86bf6 --- /dev/null +++ b/othello/apps/rating/migrations/0006_alter_gauntlet_myside2.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-05-26 19:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0005_auto_20240526_1510'), + ] + + operations = [ + migrations.AlterField( + model_name='gauntlet', + name='mySide2', + field=models.CharField(default='o', max_length=1), + ), + ] diff --git a/othello/apps/rating/migrations/0007_auto_20240526_1512.py b/othello/apps/rating/migrations/0007_auto_20240526_1512.py new file mode 100644 index 000000000..c3854e1af --- /dev/null +++ b/othello/apps/rating/migrations/0007_auto_20240526_1512.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-05-26 19:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0006_alter_gauntlet_myside2'), + ] + + operations = [ + migrations.AlterField( + model_name='gauntlet', + name='mySide1', + field=models.CharField(default='x', max_length=1), + ), + migrations.AlterField( + model_name='gauntlet', + name='mySide3', + field=models.CharField(default='o', max_length=1), + ), + ] diff --git a/othello/apps/rating/migrations/0008_auto_20240526_1514.py b/othello/apps/rating/migrations/0008_auto_20240526_1514.py new file mode 100644 index 000000000..0e748d339 --- /dev/null +++ b/othello/apps/rating/migrations/0008_auto_20240526_1514.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-05-26 19:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0007_auto_20240526_1512'), + ] + + operations = [ + migrations.AlterField( + model_name='gauntlet', + name='mySide2', + field=models.CharField(default='x', max_length=1), + ), + migrations.AlterField( + model_name='gauntlet', + name='mySide3', + field=models.CharField(default='x', max_length=1), + ), + ] diff --git a/othello/apps/rating/migrations/0009_auto_20240526_1536.py b/othello/apps/rating/migrations/0009_auto_20240526_1536.py new file mode 100644 index 000000000..818576798 --- /dev/null +++ b/othello/apps/rating/migrations/0009_auto_20240526_1536.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.25 on 2024-05-26 19:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0008_auto_20240526_1514'), + ] + + 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(auto_now=True)), + ('running', models.BooleanField(default=False)), + ('celery_task_id', models.CharField(default='', max_length=48)), + ], + ), + migrations.AlterField( + model_name='gauntlet', + name='mySide1', + field=models.CharField(default='o', max_length=1), + ), + migrations.AlterField( + model_name='gauntlet', + name='mySide3', + field=models.CharField(default='o', max_length=1), + ), + ] diff --git a/othello/apps/rating/migrations/0010_alter_gauntlet_myside3.py b/othello/apps/rating/migrations/0010_alter_gauntlet_myside3.py new file mode 100644 index 000000000..45ff0e1b5 --- /dev/null +++ b/othello/apps/rating/migrations/0010_alter_gauntlet_myside3.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-05-26 23:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0009_auto_20240526_1536'), + ] + + operations = [ + migrations.AlterField( + model_name='gauntlet', + name='mySide3', + field=models.CharField(default='x', max_length=1), + ), + ] diff --git a/othello/apps/rating/migrations/0011_auto_20240527_2119.py b/othello/apps/rating/migrations/0011_auto_20240527_2119.py new file mode 100644 index 000000000..7f318387a --- /dev/null +++ b/othello/apps/rating/migrations/0011_auto_20240527_2119.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.25 on 2024-05-28 01:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0010_alter_gauntlet_myside3'), + ] + + operations = [ + migrations.AlterField( + model_name='gauntlet', + name='mySide1', + field=models.CharField(default='x', max_length=1), + ), + migrations.AlterField( + model_name='gauntlet', + name='mySide2', + field=models.CharField(default='o', max_length=1), + ), + migrations.AlterField( + model_name='gauntlet', + name='mySide3', + field=models.CharField(default='o', max_length=1), + ), + migrations.AlterField( + model_name='rankedmanager', + name='next_auto_run', + field=models.DateTimeField(), + ), + ] diff --git a/othello/apps/rating/migrations/0012_gauntlet_pastrating.py b/othello/apps/rating/migrations/0012_gauntlet_pastrating.py new file mode 100644 index 000000000..1ce48cf6f --- /dev/null +++ b/othello/apps/rating/migrations/0012_gauntlet_pastrating.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-05-28 02:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0011_auto_20240527_2119'), + ] + + operations = [ + migrations.AddField( + model_name='gauntlet', + name='pastRating', + field=models.IntegerField(default=400), + ), + ] diff --git a/othello/apps/rating/migrations/0013_gauntlet_celery_task_id.py b/othello/apps/rating/migrations/0013_gauntlet_celery_task_id.py new file mode 100644 index 000000000..e1273efdb --- /dev/null +++ b/othello/apps/rating/migrations/0013_gauntlet_celery_task_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-05-28 02:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0012_gauntlet_pastrating'), + ] + + operations = [ + migrations.AddField( + model_name='gauntlet', + name='celery_task_id', + field=models.CharField(default='', max_length=48), + ), + ] diff --git a/othello/apps/rating/migrations/0014_alter_gauntlet_myside2.py b/othello/apps/rating/migrations/0014_alter_gauntlet_myside2.py new file mode 100644 index 000000000..b6cd79fc4 --- /dev/null +++ b/othello/apps/rating/migrations/0014_alter_gauntlet_myside2.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 = [ + ('rating', '0013_gauntlet_celery_task_id'), + ] + + operations = [ + migrations.AlterField( + model_name='gauntlet', + name='mySide2', + field=models.CharField(default='x', max_length=1), + ), + ] diff --git a/othello/apps/rating/migrations/0015_auto_20240528_0833.py b/othello/apps/rating/migrations/0015_auto_20240528_0833.py new file mode 100644 index 000000000..cfdb16505 --- /dev/null +++ b/othello/apps/rating/migrations/0015_auto_20240528_0833.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-05-28 12:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rating', '0014_alter_gauntlet_myside2'), + ] + + operations = [ + migrations.AlterField( + model_name='gauntlet', + name='mySide1', + field=models.CharField(default='o', max_length=1), + ), + migrations.AlterField( + model_name='gauntlet', + name='mySide3', + field=models.CharField(default='x', max_length=1), + ), + ] diff --git a/othello/apps/rating/migrations/__init__.py b/othello/apps/rating/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/othello/apps/rating/models.py b/othello/apps/rating/models.py new file mode 100644 index 000000000..8a6127e3d --- /dev/null +++ b/othello/apps/rating/models.py @@ -0,0 +1,76 @@ +from typing import Any + +from django.db import models +from django.utils.timezone import now + +from ..games.models import Game, Submission +from ..games.validators import validate_game_time_limit +from ..auth.models import User +#from .validators import validate_tournament_rounds + +from django.contrib.auth import get_user_model + +import random + +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) + celery_task_id = models.CharField(max_length=48, default="") + + 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()}" \ No newline at end of file diff --git a/othello/apps/rating/tasks.py b/othello/apps/rating/tasks.py new file mode 100644 index 000000000..a9d40626a --- /dev/null +++ b/othello/apps/rating/tasks.py @@ -0,0 +1,292 @@ +import logging, random, time, sys +from datetime import datetime, timedelta +from typing import Optional + +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 ..games.models import Submission, Game +from ..auth.models import User +from .models import Gauntlet, RankedManager +from ..games.tasks import run_game + +from typing import Iterator, List, Tuple, TypeVar +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() + +def getNextScrimTime(): + #batches happen monday, wednesday, friday, 4pm + today = datetime.today() + mon = (-today.weekday() + 7) % 7 + wed = (2-today.weekday()+7) % 7 + fri = (4-today.weekday()+7) % 7 + + if mon == 0 or wed == 0 or fri == 0: + if datetime.now().hour >= 16: # if its after 4pm, then we dont want to run today + if mon == 0: mon = 1000 + if wed == 0: wed = 1000 + if fri == 0: fri = 1000 + + if mon < wed and mon < fri: + logger.warning("Next run is monday") + today += timedelta(days=mon) + elif wed < mon and wed < fri: + logger.warning("Next run is wednesday") + today += timedelta(days=wed) + elif fri < mon and fri < wed: + logger.warning("Next run is friday") + today += timedelta(days=fri) + + today = today.replace(hour=16, minute=0, second=0, microsecond=0) + + return today + +@shared_task +def runAllScrims(): + manager = RankedManager.objects.first() + if manager.running: + logger.warning("Did not run a batch since one already is running") + return + + manager.running = True + manager.save() + + deleteAllRankedGames() + + players = Submission.objects.rated() + matches = doPairing(players.count()) + submissions = list(players) + + # print(matches, submissions) + + for round_matches in chunks(matches, 1 if sys.platform == "win32" else settings.CONCURRENT_GAME_LIMIT): + games = [] + + for i, j in round_matches: + submissions[i].refresh_from_db() + submissions[j].refresh_from_db() + game = Game.objects.create( + black=submissions[i], + white=submissions[j], + blackRating=submissions[i].rating, + whiteRating=submissions[j].rating, + time_limit=5, + playing=True, + is_ranked=True + ) + games.append(game) + logger.warning(f"Upcoming matches: {str(game)}") + run_game.delay(game.id) + + # tasks = {game: run_game.delay(game.id) for game in games} + # return + + for it in range(1000): # if this doesn't finish in 15 minutes, just moves on + running = False + for game in games: + #tasks = {game: run_tournament_game.delay(game.id) for game in games} + game.refresh_from_db() + running = running or game.playing + # print("check ", running) + if not running: break + time.sleep(1) + + running = False + for game in games: + running = running or game.playing + if running: + logger.warn("Some games didn't finish. Very bad.") + + for game in games: + 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 + + # THIS IS NOT TESTED - has the precondition that this is the only task queued, so its safe to queue another runAllScrims + if manager.auto_run: + logger.warning("queueing batch for later") + manager.next_auto_run = getNextScrimTime() + task = runAllScrims.apply_async([], eta=manager.next_auto_run) + manager.celery_task_id = task.id + + manager.save() + return \ No newline at end of file diff --git a/othello/apps/rating/templatetags/__init__.py b/othello/apps/rating/templatetags/__init__.py new file mode 100644 index 000000000..a2953361e --- /dev/null +++ b/othello/apps/rating/templatetags/__init__.py @@ -0,0 +1 @@ +#todo, make ranked games seperate from gauntlet games \ No newline at end of file diff --git a/othello/apps/rating/templatetags/extras.py b/othello/apps/rating/templatetags/extras.py new file mode 100644 index 000000000..43cc8f546 --- /dev/null +++ b/othello/apps/rating/templatetags/extras.py @@ -0,0 +1,28 @@ +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)}" \ No newline at end of file diff --git a/othello/apps/rating/urls.py b/othello/apps/rating/urls.py new file mode 100644 index 000000000..6969e6abc --- /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 000000000..00bd930b8 --- /dev/null +++ b/othello/apps/rating/views.py @@ -0,0 +1,241 @@ +import json, sys +import logging +from datetime import timedelta, datetime +import pytz + +from django.contrib.auth.decorators import login_required +from ..auth.decorators import management_only +from django.http import FileResponse, HttpRequest, HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone +from django.core.paginator import Paginator + +from celery.result import AsyncResult + +from ..games.models import Submission, Game +from .tasks import runGauntlet, runAllScrims, deleteAllRankedGames, getNextScrimTime +from .models import Gauntlet, RankedManager +from .forms import MultipleChoiceForm + +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() + game1 = Game.objects.create( + black=submission, + white=submission, + time_limit=5, + playing=False, + last_heartbeat=timezone.now(), + runoff=False, + is_gauntlet=True, + ) + game2 = Game.objects.create( + black=submission, + white=submission, + time_limit=5, + playing=False, + last_heartbeat=timezone.now(), + runoff=False, + is_gauntlet=True, + ) + game3 = Game.objects.create( + black=submission, + white=submission, + 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.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).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'] + #print(selected_choice) + if selected_choice == 'runbatch': + AsyncResult(manager.celery_task_id).revoke(terminate=True) + if sys.platform == 'win32': + runAllScrims() + else: + runAllScrims.delay() + elif selected_choice == 'deletegames': + deleteAllRankedGames() + elif selected_choice == 'deletegauntlets': + Gauntlet.objects.all().delete() + elif selected_choice == 'disableauto': + manager.auto_run = False + AsyncResult(manager.celery_task_id).revoke(terminate=True) + manager.save() + + # print(AsyncResult(manager.celery_task_id).failed()) + # print(AsyncResult(manager.celery_task_id).state) + + manager.save() + elif selected_choice == 'enableauto': + manager.auto_run = True + manager.next_auto_run = getNextScrimTime() + + if AsyncResult(manager.celery_task_id).state != "PENDING": + logger.warning("queueing batch for later") + task = runAllScrims.apply_async([], eta=manager.next_auto_run) + manager.celery_task_id = task.id + manager.save() + + + else: + form = MultipleChoiceForm() + return render(request, 'rating/manage.html', {'form': form, "manager": manager}) diff --git a/othello/moderator/runners.py b/othello/moderator/runners.py index 66187b7a4..6bddf61c5 100644 --- a/othello/moderator/runners.py +++ b/othello/moderator/runners.py @@ -3,6 +3,7 @@ import signal import subprocess import time +import sys from typing import Generator, Tuple, Union import psutil @@ -44,7 +45,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.stop() def start(self): - cmd_args = ["python3", "-u", self.driver, self.path] + if sys.platform == "win32": + cmd_args = ["python", "-u", self.driver, self.path] + else: + cmd_args = ["python3", "-u", self.driver, self.path] if not settings.DEBUG: cmd_args = get_sandbox_args( cmd_args, @@ -54,26 +58,39 @@ def start(self): ], # WARNING: Making the submission directory writable creates potential for extremely dangerous symlink attacks ) - self.process = subprocess.Popen( - cmd_args, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - bufsize=0, - cwd=os.path.dirname(self.path), - preexec_fn=os.setpgrp, - ) + if sys.platform == "win32": #windows does not have os.setpgrp + self.process = subprocess.Popen( + cmd_args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=0, + cwd=os.path.dirname(self.path), + # preexec_fn=os.setpgrp, + ) + else: + self.process = subprocess.Popen( + cmd_args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=0, + cwd=os.path.dirname(self.path), + preexec_fn=os.setpgrp, + ) def stop(self): if self.process is not None: try: children = psutil.Process(self.process.pid).children(recursive=True) - os.killpg(self.process.pid, signal.SIGKILL) + if sys.platform != "win32": + os.killpg(self.process.pid, signal.SIGKILL) except psutil.NoSuchProcess: self.process = None return except ProcessLookupError: self.process.kill() + for child in children: try: child.kill() @@ -85,6 +102,7 @@ def stop(self): def get_move( self, board: str, player: Player, time_limit: int, last_move: Move ) -> Generator[str, None, Union[Tuple[int, int, int], Tuple[int, ServerError, int], Tuple[int, UserError, int]], ]: + if self.process.poll(): print(self.process.communicate()) return -1, ServerError.PROCESS_EXITED, -1 @@ -99,31 +117,71 @@ def get_move( self.process.stdin.flush() move, extra_time = -1, 0 - start, total_timeout = time.time(), time_limit + 10 - while move == -1: - if self.process.poll(): - print(self.process.communicate()) - return -1, ServerError.PROCESS_EXITED, -1 - if (timeout := total_timeout - (time.time() - start)) <= 0: - return -1, ServerError.TIMEOUT, -1 - - files_ready = select.select([self.process.stdout, self.process.stderr], [], [], timeout)[0] - if self.process.stderr in files_ready: - yield self.process.stderr.read(8192).decode("latin-1") - if self.process.stdout in files_ready: - try: - parts = self.process.stdout.readline().decode("latin-1").split(";") - move, extra_time = int(parts[0]), int(parts[1]) - print(f"GOT MOVE {player} {move};{extra_time}") - - if self.is_legacy: - if move not in MOVES_10x10: - return -1, UserError.READ_INVALID, -1 - else: - if move < 0 or move >= 64: - return -1, UserError.READ_INVALID, -1 - except (ValueError, IndexError): + if sys.platform == "win32": + #behold, chatgpt + def possible_moves(board_str, player): + def is_on_board(x, y): + return 0 <= x < 8 and 0 <= y < 8 + + def opponent(player): + return 'o' if player == 'x' else 'x' + + # Convert the single string into a 2D list (8x8 board) + board = [list(board_str[i:i + 8]) for i in range(0, 64, 8)] + directions = [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)] + possible_moves = [] + + for x in range(8): + for y in range(8): + if board[x][y] == '.': + for dx, dy in directions: + nx, ny = x + dx, y + dy + if is_on_board(nx, ny) and board[nx][ny] == opponent(player): + while is_on_board(nx, ny) and board[nx][ny] == opponent(player): + nx += dx + ny += dy + if is_on_board(nx, ny) and board[nx][ny] == player: + possible_moves.append(y+x*8) + break + + return possible_moves + + import random + move = random.choice(possible_moves(board, player)) + + if self.is_legacy: + if move not in MOVES_10x10: + return -1, UserError.READ_INVALID, -1 + else: + if move < 0 or move >= 64: return -1, UserError.READ_INVALID, -1 + else: + start, total_timeout = time.time(), time_limit + 10 + while move == -1: + if self.process.poll(): + print(self.process.communicate()) + return -1, ServerError.PROCESS_EXITED, -1 + if (timeout := total_timeout - (time.time() - start)) <= 0: + return -1, ServerError.TIMEOUT, -1 + + files_ready = select.select([self.process.stdout, self.process.stderr], [], [], timeout)[0] + if self.process.stderr in files_ready: + yield self.process.stderr.read(8192).decode("latin-1") + if self.process.stdout in files_ready: + try: + parts = self.process.stdout.readline().decode("latin-1").split(";") + move, extra_time = int(parts[0]), int(parts[1]) + print(f"GOT MOVE {player} {move};{extra_time}") + + if self.is_legacy: + if move not in MOVES_10x10: + return -1, UserError.READ_INVALID, -1 + else: + if move < 0 or move >= 64: + return -1, UserError.READ_INVALID, -1 + except (ValueError, IndexError): + return -1, UserError.READ_INVALID, -1 + return (move, 0, extra_time) if not self.is_legacy else (MOVES_10x10[move], 0, extra_time) diff --git a/othello/sandboxing/__init__.py b/othello/sandboxing/__init__.py index 0d2b2d56b..fba4ea05a 100644 --- a/othello/sandboxing/__init__.py +++ b/othello/sandboxing/__init__.py @@ -1,6 +1,6 @@ import json import logging -import os +import os, sys import subprocess import traceback from typing import Dict, List, Optional @@ -11,6 +11,9 @@ def import_strategy_sandboxed(path: str) -> Optional[Dict[str, str]]: + if sys.platform == "win32": + return None + cmd_args = ["python3", "-u", settings.IMPORT_DRIVER, path] if not settings.DEBUG: cmd_args = get_sandbox_args(cmd_args, whitelist=[os.path.dirname(path)], readonly=[os.path.dirname(path)]) @@ -61,4 +64,4 @@ def get_sandbox_args( if extra_args: firejail_args.extend(extra_args) - return [*firejail_args, *cmd_args] + return [*firejail_args, *cmd_args] \ No newline at end of file diff --git a/othello/sandboxing/import_wrapper.py b/othello/sandboxing/import_wrapper.py index f0205af69..8f3bddf69 100644 --- a/othello/sandboxing/import_wrapper.py +++ b/othello/sandboxing/import_wrapper.py @@ -26,4 +26,4 @@ def main() -> None: if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/othello/settings/__init__.py b/othello/settings/__init__.py index 42e6e6819..725802089 100644 --- a/othello/settings/__init__.py +++ b/othello/settings/__init__.py @@ -44,6 +44,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/settings/secret.sample.py b/othello/settings/secret.sample.py index 4c6734439..b33c42d12 100644 --- a/othello/settings/secret.sample.py +++ b/othello/settings/secret.sample.py @@ -39,4 +39,4 @@ FRONT_PAGE_MESSAGE = "" # Message to display on every page. HTML not escaped, be careful. -GLOBAL_MESSAGE = "" +GLOBAL_MESSAGE = "" \ No newline at end of file diff --git a/othello/static/js/base.js b/othello/static/js/base.js index a7e190266..857c900bb 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 fd64f4578..8771acd8c 100644 --- a/othello/templates/base.html +++ b/othello/templates/base.html @@ -58,6 +58,20 @@ {% endif %} + {% if user|has_management_permissions %}