From 2cbccd4d01d6b2b56afea201e3d26734f0c429a5 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 12:06:55 -0400 Subject: [PATCH 01/35] Implemented webhook on solve --- ...35_contest_first_blood_webhook_and_more.py | 638 ++++++++++++++++++ gameserver/models/contest.py | 45 ++ poetry.lock | 418 +++++++++++- pyproject.toml | 1 + 4 files changed, 1101 insertions(+), 1 deletion(-) create mode 100644 gameserver/migrations/0035_contest_first_blood_webhook_and_more.py diff --git a/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py b/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py new file mode 100644 index 0000000..1e39535 --- /dev/null +++ b/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py @@ -0,0 +1,638 @@ +# Generated by Django 5.0.4 on 2024-04-06 16:06 + +import gameserver.models.profile +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("gameserver", "0034_alter_contestscore_options_alter_userscore_options_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="contest", + name="first_blood_webhook", + field=models.URLField( + blank=True, + help_text="URL to send a POST request to when a user gets the first blood on a problem", + ), + ), + migrations.AlterField( + model_name="contest", + name="organizations", + field=models.ManyToManyField( + blank=True, + help_text="Only users of these organizations can access the contest", + related_name="contests", + to="gameserver.organization", + ), + ), + migrations.AlterField( + model_name="user", + name="timezone", + field=models.CharField( + choices=[ + ("Africa/Abidjan", "Africa/Abidjan"), + ("Africa/Accra", "Africa/Accra"), + ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), + ("Africa/Algiers", "Africa/Algiers"), + ("Africa/Asmara", "Africa/Asmara"), + ("Africa/Asmera", "Africa/Asmera"), + ("Africa/Bamako", "Africa/Bamako"), + ("Africa/Bangui", "Africa/Bangui"), + ("Africa/Banjul", "Africa/Banjul"), + ("Africa/Bissau", "Africa/Bissau"), + ("Africa/Blantyre", "Africa/Blantyre"), + ("Africa/Brazzaville", "Africa/Brazzaville"), + ("Africa/Bujumbura", "Africa/Bujumbura"), + ("Africa/Cairo", "Africa/Cairo"), + ("Africa/Casablanca", "Africa/Casablanca"), + ("Africa/Ceuta", "Africa/Ceuta"), + ("Africa/Conakry", "Africa/Conakry"), + ("Africa/Dakar", "Africa/Dakar"), + ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), + ("Africa/Djibouti", "Africa/Djibouti"), + ("Africa/Douala", "Africa/Douala"), + ("Africa/El_Aaiun", "Africa/El_Aaiun"), + ("Africa/Freetown", "Africa/Freetown"), + ("Africa/Gaborone", "Africa/Gaborone"), + ("Africa/Harare", "Africa/Harare"), + ("Africa/Johannesburg", "Africa/Johannesburg"), + ("Africa/Juba", "Africa/Juba"), + ("Africa/Kampala", "Africa/Kampala"), + ("Africa/Khartoum", "Africa/Khartoum"), + ("Africa/Kigali", "Africa/Kigali"), + ("Africa/Kinshasa", "Africa/Kinshasa"), + ("Africa/Lagos", "Africa/Lagos"), + ("Africa/Libreville", "Africa/Libreville"), + ("Africa/Lome", "Africa/Lome"), + ("Africa/Luanda", "Africa/Luanda"), + ("Africa/Lubumbashi", "Africa/Lubumbashi"), + ("Africa/Lusaka", "Africa/Lusaka"), + ("Africa/Malabo", "Africa/Malabo"), + ("Africa/Maputo", "Africa/Maputo"), + ("Africa/Maseru", "Africa/Maseru"), + ("Africa/Mbabane", "Africa/Mbabane"), + ("Africa/Mogadishu", "Africa/Mogadishu"), + ("Africa/Monrovia", "Africa/Monrovia"), + ("Africa/Nairobi", "Africa/Nairobi"), + ("Africa/Ndjamena", "Africa/Ndjamena"), + ("Africa/Niamey", "Africa/Niamey"), + ("Africa/Nouakchott", "Africa/Nouakchott"), + ("Africa/Ouagadougou", "Africa/Ouagadougou"), + ("Africa/Porto-Novo", "Africa/Porto-Novo"), + ("Africa/Sao_Tome", "Africa/Sao_Tome"), + ("Africa/Timbuktu", "Africa/Timbuktu"), + ("Africa/Tripoli", "Africa/Tripoli"), + ("Africa/Tunis", "Africa/Tunis"), + ("Africa/Windhoek", "Africa/Windhoek"), + ("America/Adak", "America/Adak"), + ("America/Anchorage", "America/Anchorage"), + ("America/Anguilla", "America/Anguilla"), + ("America/Antigua", "America/Antigua"), + ("America/Araguaina", "America/Araguaina"), + ("America/Argentina/Buenos_Aires", "America/Argentina/Buenos_Aires"), + ("America/Argentina/Catamarca", "America/Argentina/Catamarca"), + ("America/Argentina/ComodRivadavia", "America/Argentina/ComodRivadavia"), + ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), + ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), + ("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"), + ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), + ("America/Argentina/Rio_Gallegos", "America/Argentina/Rio_Gallegos"), + ("America/Argentina/Salta", "America/Argentina/Salta"), + ("America/Argentina/San_Juan", "America/Argentina/San_Juan"), + ("America/Argentina/San_Luis", "America/Argentina/San_Luis"), + ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), + ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), + ("America/Aruba", "America/Aruba"), + ("America/Asuncion", "America/Asuncion"), + ("America/Atikokan", "America/Atikokan"), + ("America/Atka", "America/Atka"), + ("America/Bahia", "America/Bahia"), + ("America/Bahia_Banderas", "America/Bahia_Banderas"), + ("America/Barbados", "America/Barbados"), + ("America/Belem", "America/Belem"), + ("America/Belize", "America/Belize"), + ("America/Blanc-Sablon", "America/Blanc-Sablon"), + ("America/Boa_Vista", "America/Boa_Vista"), + ("America/Bogota", "America/Bogota"), + ("America/Boise", "America/Boise"), + ("America/Buenos_Aires", "America/Buenos_Aires"), + ("America/Cambridge_Bay", "America/Cambridge_Bay"), + ("America/Campo_Grande", "America/Campo_Grande"), + ("America/Cancun", "America/Cancun"), + ("America/Caracas", "America/Caracas"), + ("America/Catamarca", "America/Catamarca"), + ("America/Cayenne", "America/Cayenne"), + ("America/Cayman", "America/Cayman"), + ("America/Chicago", "America/Chicago"), + ("America/Chihuahua", "America/Chihuahua"), + ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), + ("America/Coral_Harbour", "America/Coral_Harbour"), + ("America/Cordoba", "America/Cordoba"), + ("America/Costa_Rica", "America/Costa_Rica"), + ("America/Creston", "America/Creston"), + ("America/Cuiaba", "America/Cuiaba"), + ("America/Curacao", "America/Curacao"), + ("America/Danmarkshavn", "America/Danmarkshavn"), + ("America/Dawson", "America/Dawson"), + ("America/Dawson_Creek", "America/Dawson_Creek"), + ("America/Denver", "America/Denver"), + ("America/Detroit", "America/Detroit"), + ("America/Dominica", "America/Dominica"), + ("America/Edmonton", "America/Edmonton"), + ("America/Eirunepe", "America/Eirunepe"), + ("America/El_Salvador", "America/El_Salvador"), + ("America/Ensenada", "America/Ensenada"), + ("America/Fort_Nelson", "America/Fort_Nelson"), + ("America/Fort_Wayne", "America/Fort_Wayne"), + ("America/Fortaleza", "America/Fortaleza"), + ("America/Glace_Bay", "America/Glace_Bay"), + ("America/Godthab", "America/Godthab"), + ("America/Goose_Bay", "America/Goose_Bay"), + ("America/Grand_Turk", "America/Grand_Turk"), + ("America/Grenada", "America/Grenada"), + ("America/Guadeloupe", "America/Guadeloupe"), + ("America/Guatemala", "America/Guatemala"), + ("America/Guayaquil", "America/Guayaquil"), + ("America/Guyana", "America/Guyana"), + ("America/Halifax", "America/Halifax"), + ("America/Havana", "America/Havana"), + ("America/Hermosillo", "America/Hermosillo"), + ("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"), + ("America/Indiana/Knox", "America/Indiana/Knox"), + ("America/Indiana/Marengo", "America/Indiana/Marengo"), + ("America/Indiana/Petersburg", "America/Indiana/Petersburg"), + ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), + ("America/Indiana/Vevay", "America/Indiana/Vevay"), + ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), + ("America/Indiana/Winamac", "America/Indiana/Winamac"), + ("America/Indianapolis", "America/Indianapolis"), + ("America/Inuvik", "America/Inuvik"), + ("America/Iqaluit", "America/Iqaluit"), + ("America/Jamaica", "America/Jamaica"), + ("America/Jujuy", "America/Jujuy"), + ("America/Juneau", "America/Juneau"), + ("America/Kentucky/Louisville", "America/Kentucky/Louisville"), + ("America/Kentucky/Monticello", "America/Kentucky/Monticello"), + ("America/Knox_IN", "America/Knox_IN"), + ("America/Kralendijk", "America/Kralendijk"), + ("America/La_Paz", "America/La_Paz"), + ("America/Lima", "America/Lima"), + ("America/Los_Angeles", "America/Los_Angeles"), + ("America/Louisville", "America/Louisville"), + ("America/Lower_Princes", "America/Lower_Princes"), + ("America/Maceio", "America/Maceio"), + ("America/Managua", "America/Managua"), + ("America/Manaus", "America/Manaus"), + ("America/Marigot", "America/Marigot"), + ("America/Martinique", "America/Martinique"), + ("America/Matamoros", "America/Matamoros"), + ("America/Mazatlan", "America/Mazatlan"), + ("America/Mendoza", "America/Mendoza"), + ("America/Menominee", "America/Menominee"), + ("America/Merida", "America/Merida"), + ("America/Metlakatla", "America/Metlakatla"), + ("America/Mexico_City", "America/Mexico_City"), + ("America/Miquelon", "America/Miquelon"), + ("America/Moncton", "America/Moncton"), + ("America/Monterrey", "America/Monterrey"), + ("America/Montevideo", "America/Montevideo"), + ("America/Montreal", "America/Montreal"), + ("America/Montserrat", "America/Montserrat"), + ("America/Nassau", "America/Nassau"), + ("America/New_York", "America/New_York"), + ("America/Nipigon", "America/Nipigon"), + ("America/Nome", "America/Nome"), + ("America/Noronha", "America/Noronha"), + ("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"), + ("America/North_Dakota/Center", "America/North_Dakota/Center"), + ("America/North_Dakota/New_Salem", "America/North_Dakota/New_Salem"), + ("America/Nuuk", "America/Nuuk"), + ("America/Ojinaga", "America/Ojinaga"), + ("America/Panama", "America/Panama"), + ("America/Pangnirtung", "America/Pangnirtung"), + ("America/Paramaribo", "America/Paramaribo"), + ("America/Phoenix", "America/Phoenix"), + ("America/Port-au-Prince", "America/Port-au-Prince"), + ("America/Port_of_Spain", "America/Port_of_Spain"), + ("America/Porto_Acre", "America/Porto_Acre"), + ("America/Porto_Velho", "America/Porto_Velho"), + ("America/Puerto_Rico", "America/Puerto_Rico"), + ("America/Punta_Arenas", "America/Punta_Arenas"), + ("America/Rainy_River", "America/Rainy_River"), + ("America/Rankin_Inlet", "America/Rankin_Inlet"), + ("America/Recife", "America/Recife"), + ("America/Regina", "America/Regina"), + ("America/Resolute", "America/Resolute"), + ("America/Rio_Branco", "America/Rio_Branco"), + ("America/Rosario", "America/Rosario"), + ("America/Santa_Isabel", "America/Santa_Isabel"), + ("America/Santarem", "America/Santarem"), + ("America/Santiago", "America/Santiago"), + ("America/Santo_Domingo", "America/Santo_Domingo"), + ("America/Sao_Paulo", "America/Sao_Paulo"), + ("America/Scoresbysund", "America/Scoresbysund"), + ("America/Shiprock", "America/Shiprock"), + ("America/Sitka", "America/Sitka"), + ("America/St_Barthelemy", "America/St_Barthelemy"), + ("America/St_Johns", "America/St_Johns"), + ("America/St_Kitts", "America/St_Kitts"), + ("America/St_Lucia", "America/St_Lucia"), + ("America/St_Thomas", "America/St_Thomas"), + ("America/St_Vincent", "America/St_Vincent"), + ("America/Swift_Current", "America/Swift_Current"), + ("America/Tegucigalpa", "America/Tegucigalpa"), + ("America/Thule", "America/Thule"), + ("America/Thunder_Bay", "America/Thunder_Bay"), + ("America/Tijuana", "America/Tijuana"), + ("America/Toronto", "America/Toronto"), + ("America/Tortola", "America/Tortola"), + ("America/Vancouver", "America/Vancouver"), + ("America/Virgin", "America/Virgin"), + ("America/Whitehorse", "America/Whitehorse"), + ("America/Winnipeg", "America/Winnipeg"), + ("America/Yakutat", "America/Yakutat"), + ("America/Yellowknife", "America/Yellowknife"), + ("Antarctica/Casey", "Antarctica/Casey"), + ("Antarctica/Davis", "Antarctica/Davis"), + ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), + ("Antarctica/Macquarie", "Antarctica/Macquarie"), + ("Antarctica/Mawson", "Antarctica/Mawson"), + ("Antarctica/McMurdo", "Antarctica/McMurdo"), + ("Antarctica/Palmer", "Antarctica/Palmer"), + ("Antarctica/Rothera", "Antarctica/Rothera"), + ("Antarctica/South_Pole", "Antarctica/South_Pole"), + ("Antarctica/Syowa", "Antarctica/Syowa"), + ("Antarctica/Troll", "Antarctica/Troll"), + ("Antarctica/Vostok", "Antarctica/Vostok"), + ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), + ("Asia/Aden", "Asia/Aden"), + ("Asia/Almaty", "Asia/Almaty"), + ("Asia/Amman", "Asia/Amman"), + ("Asia/Anadyr", "Asia/Anadyr"), + ("Asia/Aqtau", "Asia/Aqtau"), + ("Asia/Aqtobe", "Asia/Aqtobe"), + ("Asia/Ashgabat", "Asia/Ashgabat"), + ("Asia/Ashkhabad", "Asia/Ashkhabad"), + ("Asia/Atyrau", "Asia/Atyrau"), + ("Asia/Baghdad", "Asia/Baghdad"), + ("Asia/Bahrain", "Asia/Bahrain"), + ("Asia/Baku", "Asia/Baku"), + ("Asia/Bangkok", "Asia/Bangkok"), + ("Asia/Barnaul", "Asia/Barnaul"), + ("Asia/Beirut", "Asia/Beirut"), + ("Asia/Bishkek", "Asia/Bishkek"), + ("Asia/Brunei", "Asia/Brunei"), + ("Asia/Calcutta", "Asia/Calcutta"), + ("Asia/Chita", "Asia/Chita"), + ("Asia/Choibalsan", "Asia/Choibalsan"), + ("Asia/Chongqing", "Asia/Chongqing"), + ("Asia/Chungking", "Asia/Chungking"), + ("Asia/Colombo", "Asia/Colombo"), + ("Asia/Dacca", "Asia/Dacca"), + ("Asia/Damascus", "Asia/Damascus"), + ("Asia/Dhaka", "Asia/Dhaka"), + ("Asia/Dili", "Asia/Dili"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Dushanbe", "Asia/Dushanbe"), + ("Asia/Famagusta", "Asia/Famagusta"), + ("Asia/Gaza", "Asia/Gaza"), + ("Asia/Harbin", "Asia/Harbin"), + ("Asia/Hebron", "Asia/Hebron"), + ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), + ("Asia/Hong_Kong", "Asia/Hong_Kong"), + ("Asia/Hovd", "Asia/Hovd"), + ("Asia/Irkutsk", "Asia/Irkutsk"), + ("Asia/Istanbul", "Asia/Istanbul"), + ("Asia/Jakarta", "Asia/Jakarta"), + ("Asia/Jayapura", "Asia/Jayapura"), + ("Asia/Jerusalem", "Asia/Jerusalem"), + ("Asia/Kabul", "Asia/Kabul"), + ("Asia/Kamchatka", "Asia/Kamchatka"), + ("Asia/Karachi", "Asia/Karachi"), + ("Asia/Kashgar", "Asia/Kashgar"), + ("Asia/Kathmandu", "Asia/Kathmandu"), + ("Asia/Katmandu", "Asia/Katmandu"), + ("Asia/Khandyga", "Asia/Khandyga"), + ("Asia/Kolkata", "Asia/Kolkata"), + ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), + ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), + ("Asia/Kuching", "Asia/Kuching"), + ("Asia/Kuwait", "Asia/Kuwait"), + ("Asia/Macao", "Asia/Macao"), + ("Asia/Macau", "Asia/Macau"), + ("Asia/Magadan", "Asia/Magadan"), + ("Asia/Makassar", "Asia/Makassar"), + ("Asia/Manila", "Asia/Manila"), + ("Asia/Muscat", "Asia/Muscat"), + ("Asia/Nicosia", "Asia/Nicosia"), + ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), + ("Asia/Novosibirsk", "Asia/Novosibirsk"), + ("Asia/Omsk", "Asia/Omsk"), + ("Asia/Oral", "Asia/Oral"), + ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), + ("Asia/Pontianak", "Asia/Pontianak"), + ("Asia/Pyongyang", "Asia/Pyongyang"), + ("Asia/Qatar", "Asia/Qatar"), + ("Asia/Qostanay", "Asia/Qostanay"), + ("Asia/Qyzylorda", "Asia/Qyzylorda"), + ("Asia/Rangoon", "Asia/Rangoon"), + ("Asia/Riyadh", "Asia/Riyadh"), + ("Asia/Saigon", "Asia/Saigon"), + ("Asia/Sakhalin", "Asia/Sakhalin"), + ("Asia/Samarkand", "Asia/Samarkand"), + ("Asia/Seoul", "Asia/Seoul"), + ("Asia/Shanghai", "Asia/Shanghai"), + ("Asia/Singapore", "Asia/Singapore"), + ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), + ("Asia/Taipei", "Asia/Taipei"), + ("Asia/Tashkent", "Asia/Tashkent"), + ("Asia/Tbilisi", "Asia/Tbilisi"), + ("Asia/Tehran", "Asia/Tehran"), + ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), + ("Asia/Thimbu", "Asia/Thimbu"), + ("Asia/Thimphu", "Asia/Thimphu"), + ("Asia/Tokyo", "Asia/Tokyo"), + ("Asia/Tomsk", "Asia/Tomsk"), + ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), + ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), + ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), + ("Asia/Urumqi", "Asia/Urumqi"), + ("Asia/Ust-Nera", "Asia/Ust-Nera"), + ("Asia/Vientiane", "Asia/Vientiane"), + ("Asia/Vladivostok", "Asia/Vladivostok"), + ("Asia/Yakutsk", "Asia/Yakutsk"), + ("Asia/Yangon", "Asia/Yangon"), + ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), + ("Asia/Yerevan", "Asia/Yerevan"), + ("Atlantic/Azores", "Atlantic/Azores"), + ("Atlantic/Bermuda", "Atlantic/Bermuda"), + ("Atlantic/Canary", "Atlantic/Canary"), + ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), + ("Atlantic/Faeroe", "Atlantic/Faeroe"), + ("Atlantic/Faroe", "Atlantic/Faroe"), + ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), + ("Atlantic/Madeira", "Atlantic/Madeira"), + ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), + ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), + ("Atlantic/St_Helena", "Atlantic/St_Helena"), + ("Atlantic/Stanley", "Atlantic/Stanley"), + ("Australia/ACT", "Australia/ACT"), + ("Australia/Adelaide", "Australia/Adelaide"), + ("Australia/Brisbane", "Australia/Brisbane"), + ("Australia/Broken_Hill", "Australia/Broken_Hill"), + ("Australia/Canberra", "Australia/Canberra"), + ("Australia/Currie", "Australia/Currie"), + ("Australia/Darwin", "Australia/Darwin"), + ("Australia/Eucla", "Australia/Eucla"), + ("Australia/Hobart", "Australia/Hobart"), + ("Australia/LHI", "Australia/LHI"), + ("Australia/Lindeman", "Australia/Lindeman"), + ("Australia/Lord_Howe", "Australia/Lord_Howe"), + ("Australia/Melbourne", "Australia/Melbourne"), + ("Australia/NSW", "Australia/NSW"), + ("Australia/North", "Australia/North"), + ("Australia/Perth", "Australia/Perth"), + ("Australia/Queensland", "Australia/Queensland"), + ("Australia/South", "Australia/South"), + ("Australia/Sydney", "Australia/Sydney"), + ("Australia/Tasmania", "Australia/Tasmania"), + ("Australia/Victoria", "Australia/Victoria"), + ("Australia/West", "Australia/West"), + ("Australia/Yancowinna", "Australia/Yancowinna"), + ("Brazil/Acre", "Brazil/Acre"), + ("Brazil/DeNoronha", "Brazil/DeNoronha"), + ("Brazil/East", "Brazil/East"), + ("Brazil/West", "Brazil/West"), + ("CET", "CET"), + ("CST6CDT", "CST6CDT"), + ("Canada/Atlantic", "Canada/Atlantic"), + ("Canada/Central", "Canada/Central"), + ("Canada/Eastern", "Canada/Eastern"), + ("Canada/Mountain", "Canada/Mountain"), + ("Canada/Newfoundland", "Canada/Newfoundland"), + ("Canada/Pacific", "Canada/Pacific"), + ("Canada/Saskatchewan", "Canada/Saskatchewan"), + ("Canada/Yukon", "Canada/Yukon"), + ("Chile/Continental", "Chile/Continental"), + ("Chile/EasterIsland", "Chile/EasterIsland"), + ("Cuba", "Cuba"), + ("EET", "EET"), + ("EST", "EST"), + ("EST5EDT", "EST5EDT"), + ("Egypt", "Egypt"), + ("Eire", "Eire"), + ("Etc/GMT", "Etc/GMT"), + ("Etc/GMT+0", "Etc/GMT+0"), + ("Etc/GMT+1", "Etc/GMT+1"), + ("Etc/GMT+10", "Etc/GMT+10"), + ("Etc/GMT+11", "Etc/GMT+11"), + ("Etc/GMT+12", "Etc/GMT+12"), + ("Etc/GMT+2", "Etc/GMT+2"), + ("Etc/GMT+3", "Etc/GMT+3"), + ("Etc/GMT+4", "Etc/GMT+4"), + ("Etc/GMT+5", "Etc/GMT+5"), + ("Etc/GMT+6", "Etc/GMT+6"), + ("Etc/GMT+7", "Etc/GMT+7"), + ("Etc/GMT+8", "Etc/GMT+8"), + ("Etc/GMT+9", "Etc/GMT+9"), + ("Etc/GMT-0", "Etc/GMT-0"), + ("Etc/GMT-1", "Etc/GMT-1"), + ("Etc/GMT-10", "Etc/GMT-10"), + ("Etc/GMT-11", "Etc/GMT-11"), + ("Etc/GMT-12", "Etc/GMT-12"), + ("Etc/GMT-13", "Etc/GMT-13"), + ("Etc/GMT-14", "Etc/GMT-14"), + ("Etc/GMT-2", "Etc/GMT-2"), + ("Etc/GMT-3", "Etc/GMT-3"), + ("Etc/GMT-4", "Etc/GMT-4"), + ("Etc/GMT-5", "Etc/GMT-5"), + ("Etc/GMT-6", "Etc/GMT-6"), + ("Etc/GMT-7", "Etc/GMT-7"), + ("Etc/GMT-8", "Etc/GMT-8"), + ("Etc/GMT-9", "Etc/GMT-9"), + ("Etc/GMT0", "Etc/GMT0"), + ("Etc/Greenwich", "Etc/Greenwich"), + ("Etc/UCT", "Etc/UCT"), + ("Etc/UTC", "Etc/UTC"), + ("Etc/Universal", "Etc/Universal"), + ("Etc/Zulu", "Etc/Zulu"), + ("Europe/Amsterdam", "Europe/Amsterdam"), + ("Europe/Andorra", "Europe/Andorra"), + ("Europe/Astrakhan", "Europe/Astrakhan"), + ("Europe/Athens", "Europe/Athens"), + ("Europe/Belfast", "Europe/Belfast"), + ("Europe/Belgrade", "Europe/Belgrade"), + ("Europe/Berlin", "Europe/Berlin"), + ("Europe/Bratislava", "Europe/Bratislava"), + ("Europe/Brussels", "Europe/Brussels"), + ("Europe/Bucharest", "Europe/Bucharest"), + ("Europe/Budapest", "Europe/Budapest"), + ("Europe/Busingen", "Europe/Busingen"), + ("Europe/Chisinau", "Europe/Chisinau"), + ("Europe/Copenhagen", "Europe/Copenhagen"), + ("Europe/Dublin", "Europe/Dublin"), + ("Europe/Gibraltar", "Europe/Gibraltar"), + ("Europe/Guernsey", "Europe/Guernsey"), + ("Europe/Helsinki", "Europe/Helsinki"), + ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), + ("Europe/Istanbul", "Europe/Istanbul"), + ("Europe/Jersey", "Europe/Jersey"), + ("Europe/Kaliningrad", "Europe/Kaliningrad"), + ("Europe/Kiev", "Europe/Kiev"), + ("Europe/Kirov", "Europe/Kirov"), + ("Europe/Kyiv", "Europe/Kyiv"), + ("Europe/Lisbon", "Europe/Lisbon"), + ("Europe/Ljubljana", "Europe/Ljubljana"), + ("Europe/London", "Europe/London"), + ("Europe/Luxembourg", "Europe/Luxembourg"), + ("Europe/Madrid", "Europe/Madrid"), + ("Europe/Malta", "Europe/Malta"), + ("Europe/Mariehamn", "Europe/Mariehamn"), + ("Europe/Minsk", "Europe/Minsk"), + ("Europe/Monaco", "Europe/Monaco"), + ("Europe/Moscow", "Europe/Moscow"), + ("Europe/Nicosia", "Europe/Nicosia"), + ("Europe/Oslo", "Europe/Oslo"), + ("Europe/Paris", "Europe/Paris"), + ("Europe/Podgorica", "Europe/Podgorica"), + ("Europe/Prague", "Europe/Prague"), + ("Europe/Riga", "Europe/Riga"), + ("Europe/Rome", "Europe/Rome"), + ("Europe/Samara", "Europe/Samara"), + ("Europe/San_Marino", "Europe/San_Marino"), + ("Europe/Sarajevo", "Europe/Sarajevo"), + ("Europe/Saratov", "Europe/Saratov"), + ("Europe/Simferopol", "Europe/Simferopol"), + ("Europe/Skopje", "Europe/Skopje"), + ("Europe/Sofia", "Europe/Sofia"), + ("Europe/Stockholm", "Europe/Stockholm"), + ("Europe/Tallinn", "Europe/Tallinn"), + ("Europe/Tirane", "Europe/Tirane"), + ("Europe/Tiraspol", "Europe/Tiraspol"), + ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), + ("Europe/Uzhgorod", "Europe/Uzhgorod"), + ("Europe/Vaduz", "Europe/Vaduz"), + ("Europe/Vatican", "Europe/Vatican"), + ("Europe/Vienna", "Europe/Vienna"), + ("Europe/Vilnius", "Europe/Vilnius"), + ("Europe/Volgograd", "Europe/Volgograd"), + ("Europe/Warsaw", "Europe/Warsaw"), + ("Europe/Zagreb", "Europe/Zagreb"), + ("Europe/Zaporozhye", "Europe/Zaporozhye"), + ("Europe/Zurich", "Europe/Zurich"), + ("Factory", "Factory"), + ("GB", "GB"), + ("GB-Eire", "GB-Eire"), + ("GMT", "GMT"), + ("GMT+0", "GMT+0"), + ("GMT-0", "GMT-0"), + ("GMT0", "GMT0"), + ("Greenwich", "Greenwich"), + ("HST", "HST"), + ("Hongkong", "Hongkong"), + ("Iceland", "Iceland"), + ("Indian/Antananarivo", "Indian/Antananarivo"), + ("Indian/Chagos", "Indian/Chagos"), + ("Indian/Christmas", "Indian/Christmas"), + ("Indian/Cocos", "Indian/Cocos"), + ("Indian/Comoro", "Indian/Comoro"), + ("Indian/Kerguelen", "Indian/Kerguelen"), + ("Indian/Mahe", "Indian/Mahe"), + ("Indian/Maldives", "Indian/Maldives"), + ("Indian/Mauritius", "Indian/Mauritius"), + ("Indian/Mayotte", "Indian/Mayotte"), + ("Indian/Reunion", "Indian/Reunion"), + ("Iran", "Iran"), + ("Israel", "Israel"), + ("Jamaica", "Jamaica"), + ("Japan", "Japan"), + ("Kwajalein", "Kwajalein"), + ("Libya", "Libya"), + ("MET", "MET"), + ("MST", "MST"), + ("MST7MDT", "MST7MDT"), + ("Mexico/BajaNorte", "Mexico/BajaNorte"), + ("Mexico/BajaSur", "Mexico/BajaSur"), + ("Mexico/General", "Mexico/General"), + ("NZ", "NZ"), + ("NZ-CHAT", "NZ-CHAT"), + ("Navajo", "Navajo"), + ("PRC", "PRC"), + ("PST8PDT", "PST8PDT"), + ("Pacific/Apia", "Pacific/Apia"), + ("Pacific/Auckland", "Pacific/Auckland"), + ("Pacific/Bougainville", "Pacific/Bougainville"), + ("Pacific/Chatham", "Pacific/Chatham"), + ("Pacific/Chuuk", "Pacific/Chuuk"), + ("Pacific/Easter", "Pacific/Easter"), + ("Pacific/Efate", "Pacific/Efate"), + ("Pacific/Enderbury", "Pacific/Enderbury"), + ("Pacific/Fakaofo", "Pacific/Fakaofo"), + ("Pacific/Fiji", "Pacific/Fiji"), + ("Pacific/Funafuti", "Pacific/Funafuti"), + ("Pacific/Galapagos", "Pacific/Galapagos"), + ("Pacific/Gambier", "Pacific/Gambier"), + ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), + ("Pacific/Guam", "Pacific/Guam"), + ("Pacific/Honolulu", "Pacific/Honolulu"), + ("Pacific/Johnston", "Pacific/Johnston"), + ("Pacific/Kanton", "Pacific/Kanton"), + ("Pacific/Kiritimati", "Pacific/Kiritimati"), + ("Pacific/Kosrae", "Pacific/Kosrae"), + ("Pacific/Kwajalein", "Pacific/Kwajalein"), + ("Pacific/Majuro", "Pacific/Majuro"), + ("Pacific/Marquesas", "Pacific/Marquesas"), + ("Pacific/Midway", "Pacific/Midway"), + ("Pacific/Nauru", "Pacific/Nauru"), + ("Pacific/Niue", "Pacific/Niue"), + ("Pacific/Norfolk", "Pacific/Norfolk"), + ("Pacific/Noumea", "Pacific/Noumea"), + ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), + ("Pacific/Palau", "Pacific/Palau"), + ("Pacific/Pitcairn", "Pacific/Pitcairn"), + ("Pacific/Pohnpei", "Pacific/Pohnpei"), + ("Pacific/Ponape", "Pacific/Ponape"), + ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), + ("Pacific/Rarotonga", "Pacific/Rarotonga"), + ("Pacific/Saipan", "Pacific/Saipan"), + ("Pacific/Samoa", "Pacific/Samoa"), + ("Pacific/Tahiti", "Pacific/Tahiti"), + ("Pacific/Tarawa", "Pacific/Tarawa"), + ("Pacific/Tongatapu", "Pacific/Tongatapu"), + ("Pacific/Truk", "Pacific/Truk"), + ("Pacific/Wake", "Pacific/Wake"), + ("Pacific/Wallis", "Pacific/Wallis"), + ("Pacific/Yap", "Pacific/Yap"), + ("Poland", "Poland"), + ("Portugal", "Portugal"), + ("ROC", "ROC"), + ("ROK", "ROK"), + ("Singapore", "Singapore"), + ("Turkey", "Turkey"), + ("UCT", "UCT"), + ("US/Alaska", "US/Alaska"), + ("US/Aleutian", "US/Aleutian"), + ("US/Arizona", "US/Arizona"), + ("US/Central", "US/Central"), + ("US/East-Indiana", "US/East-Indiana"), + ("US/Eastern", "US/Eastern"), + ("US/Hawaii", "US/Hawaii"), + ("US/Indiana-Starke", "US/Indiana-Starke"), + ("US/Michigan", "US/Michigan"), + ("US/Mountain", "US/Mountain"), + ("US/Pacific", "US/Pacific"), + ("US/Samoa", "US/Samoa"), + ("UTC", "UTC"), + ("Universal", "Universal"), + ("W-SU", "W-SU"), + ("WET", "WET"), + ("Zulu", "Zulu"), + ], + default=gameserver.models.profile.get_default_user_timezone, + max_length=50, + ), + ), + ] diff --git a/gameserver/models/contest.py b/gameserver/models/contest.py index e6a6166..10b52ba 100644 --- a/gameserver/models/contest.py +++ b/gameserver/models/contest.py @@ -1,8 +1,11 @@ +import logging from datetime import timedelta +import aiohttp from django.apps import apps from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site from django.core.cache import cache from django.core.cache.utils import make_template_fragment_key from django.core.validators import MinValueValidator @@ -10,14 +13,18 @@ from django.db.models import Count, F, Max, Min, OuterRef, Q, Subquery, Sum from django.db.models.expressions import Window from django.db.models.functions import Coalesce, Rank +from django.db.models.signals import post_save +from django.dispatch import receiver from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property + from gameserver.models.cache import ContestScore from ..templatetags.common_tags import strfdelta from . import abstract +logger = logging.getLogger(__name__) # Create your models here. @@ -55,6 +62,10 @@ def __init__(self, *args, **kwargs): ) date_created = models.DateTimeField(auto_now_add=True) + first_blood_webhook = models.URLField( + blank=True, + help_text="URL to send a POST request to when a user gets the first blood on a problem", + ) class Meta: permissions = ( @@ -436,3 +447,37 @@ def save(self, *args, **kwargs): ) # see scoreboard.html ContestScore.invalidate(self.participation) super().save(*args, **kwargs) + + +def is_discord(webhook: str): + return webhook.startswith("https://discord.com/api") + + +async def construct_discord_payload(submission: ContestSubmission) -> dict: + return { + 'username': f'{submission.participation.contest.name} First Blood Notifier', + 'avatar_url': Site.objects.get_current().domain + '/static/logo.svg', + 'content': f'First blood on [{submission.problem.problem.name}]({submission.problem.get_absolute_url()} by {submission.participation.participant}!', + } + + +@receiver(post_save, sender=ContestSubmission, dispatch_uid="notify_contest_firstblood") +async def my_handler(sender, instance, created, raw, using, update_fields, **kwargs): + if not created: # only for new submissions + return + if not instance.is_firstblood: + return + + if webhook := instance.participation.contest.first_blood_webhook: + payload: dict = await construct_discord_payload(instance) + if not is_discord(webhook): + payload = payload['content'] + async with aiohttp.ClientSession() as session: + async with session.post( + webhook, + json=payload, + ) as resp: + if resp.status != 200: + text = await resp.text() + logger.error(f"Failed to send webhook: {text} - {resp.status} - {webhook} - {payload}") + diff --git a/poetry.lock b/poetry.lock index 7d3bb7d..ae44f3a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,114 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "aiohttp" +version = "3.9.3" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, + {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, + {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, + {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, + {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, + {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, + {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, + {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, + {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, + {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, + {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, + {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, + {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, + {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, + {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, + {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, + {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, + {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, + {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, + {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, + {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, + {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, + {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, + {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, + {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, + {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, + {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + [[package]] name = "annotated-types" version = "0.6.0" @@ -25,6 +134,25 @@ files = [ [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + [[package]] name = "bleach" version = "4.1.0" @@ -439,6 +567,92 @@ files = [ [package.extras] management-command = ["django-compressor (>=2.4)"] +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + [[package]] name = "google-auth" version = "2.29.0" @@ -545,6 +759,105 @@ files = [ {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, ] +[[package]] +name = "multidict" +version = "6.0.5" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] + [[package]] name = "oauthlib" version = "3.2.2" @@ -1070,7 +1383,110 @@ docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] +[[package]] +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + [metadata] lock-version = "2.0" python-versions = "^3.11.5" -content-hash = "14f8d01aaae06433e6c62905f8e94dcc88e2ea2d6f52fac65c28bebd24cc7524" +content-hash = "e2fa9973b64bbb868b271554505136edf7084539594fc4379752c10b361c69b2" diff --git a/pyproject.toml b/pyproject.toml index 9648a8b..b99bcf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ django-debug-toolbar = "^4.3.0" gunicorn = "*" django-ninja = "^1.1.0" redis = "^5.0.3" +aiohttp = "^3.9.3" [build-system] requires = ["poetry-core"] From 45e415216cc6848f3a482b4a8398fe6d48fbd2ce Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 12:07:31 -0400 Subject: [PATCH 02/35] fmt --- gameserver/admin.py | 2 +- gameserver/api/routes.py | 17 +++++++++-------- gameserver/models/cache.py | 12 +++++++----- gameserver/models/comment.py | 2 -- gameserver/urls.py | 2 +- providers/CTFTimeOauth/provider.py | 1 + providers/CTFTimeOauth/urls.py | 1 - 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/gameserver/admin.py b/gameserver/admin.py index d7866b2..c462e04 100644 --- a/gameserver/admin.py +++ b/gameserver/admin.py @@ -1,4 +1,4 @@ -from adminsortable2.admin import SortableInlineAdminMixin, SortableAdminBase +from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from django.contrib import admin from django.contrib.auth import get_user_model from django.contrib.flatpages.admin import FlatPageAdmin diff --git a/gameserver/api/routes.py b/gameserver/api/routes.py index 5d8fa8e..82781c0 100644 --- a/gameserver/api/routes.py +++ b/gameserver/api/routes.py @@ -1,12 +1,13 @@ -from django.db.models import F, OuterRef, Subquery, Case, When, Q +import datetime +from typing import Any, List + +from django.db.models import Case, F, OuterRef, Q, Subquery, When from django.shortcuts import get_object_or_404 +from ninja import NinjaAPI, Schema from gameserver.models.cache import ContestScore -from gameserver.models.contest import ContestProblem, ContestSubmission, Contest -from ninja import NinjaAPI, Schema -from typing import List, Any +from gameserver.models.contest import Contest, ContestProblem, ContestSubmission -import datetime def unicode_safe(string): return string.encode("unicode_escape").decode() @@ -85,9 +86,9 @@ def ctftime_standings(request, contest_name: str): return {"standings": standings, "tasks": task_names} + @api.get("/contests", response=List[ContestOutSchema]) def contests(request): - return ( - Contest.objects.filter(is_public=True) - .values("name", "slug", "start_time", "end_time", "max_team_size", "description", "summary") + return Contest.objects.filter(is_public=True).values( + "name", "slug", "start_time", "end_time", "max_team_size", "description", "summary" ) diff --git a/gameserver/models/cache.py b/gameserver/models/cache.py index 24b026b..db035f8 100644 --- a/gameserver/models/cache.py +++ b/gameserver/models/cache.py @@ -1,5 +1,5 @@ -from typing import TYPE_CHECKING, Optional, Self, Protocol, Callable -from django.http import HttpRequest +from typing import TYPE_CHECKING, Callable, Optional, Protocol, Self + from django.apps import apps from django.db import models, transaction from django.db.models import ( @@ -8,22 +8,24 @@ Count, F, OuterRef, + QuerySet, Subquery, Sum, Value, When, - QuerySet, Window, ) from django.db.models.functions import Coalesce, Rank, RowNumber +from django.http import HttpRequest if TYPE_CHECKING: - from .profile import User from .contest import Contest, ContestParticipation, ContestSubmission + from .profile import User class ResetableCache(Protocol): - def can_reset(cls, request: HttpRequest) -> None: ... + def can_reset(cls, request: HttpRequest) -> None: + ... class CacheMeta(models.Model): diff --git a/gameserver/models/comment.py b/gameserver/models/comment.py index 19b36d0..6efb542 100644 --- a/gameserver/models/comment.py +++ b/gameserver/models/comment.py @@ -1,9 +1,7 @@ -from typing import Iterable from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models from django.urls import reverse -from django.utils import timezone from .profile import User diff --git a/gameserver/urls.py b/gameserver/urls.py index aa74ff3..26139c3 100644 --- a/gameserver/urls.py +++ b/gameserver/urls.py @@ -1,4 +1,4 @@ -from django.urls import path, include +from django.urls import include, path from . import views from .api.routes import api diff --git a/providers/CTFTimeOauth/provider.py b/providers/CTFTimeOauth/provider.py index f4b0aa0..e4621bb 100644 --- a/providers/CTFTimeOauth/provider.py +++ b/providers/CTFTimeOauth/provider.py @@ -1,5 +1,6 @@ from allauth.socialaccount.providers.base import ProviderAccount from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider + from .views import CTFTimeOauth2Adapter diff --git a/providers/CTFTimeOauth/urls.py b/providers/CTFTimeOauth/urls.py index fd8e303..6918663 100644 --- a/providers/CTFTimeOauth/urls.py +++ b/providers/CTFTimeOauth/urls.py @@ -2,5 +2,4 @@ from .provider import CTFTimeOauth2AProvider - urlpatterns = default_urlpatterns(CTFTimeOauth2AProvider) From 6ba634a3a5ca158038d9af53218c3d8b57f22108 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 12:21:15 -0400 Subject: [PATCH 03/35] Added checks for chal cluster connection fail --- gameserver/utils/challenge.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gameserver/utils/challenge.py b/gameserver/utils/challenge.py index 8e68205..b7d2ba7 100644 --- a/gameserver/utils/challenge.py +++ b/gameserver/utils/challenge.py @@ -11,6 +11,7 @@ challenge_cluster = settings.CHALLENGE_CLUSTER +api_client = None if challenge_cluster["connection"]["host"] is not None: cluster_config = kubernetes.client.Configuration() cluster_config.host = challenge_cluster["connection"]["host"] @@ -22,6 +23,9 @@ api_client = kubernetes.dynamic.DynamicClient(kubernetes.client.ApiClient(cluster_config)) logger.info("connected to challenge cluster") +if api_client is None: + logger.error("failed to connect to challenge cluster") + raise Exception("failed to connect to challenge cluster") def create_challenge_instance(challenge_spec, problem_id, problem_flag, instance_owner, wait=False): def generate_identifier(): From cc8b3895c8f821a976b66fa3783c4ddaa4cceef2 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 12:49:40 -0400 Subject: [PATCH 04/35] Fix: attr error --- gameserver/views/contest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gameserver/views/contest.py b/gameserver/views/contest.py index adbb30b..10156d6 100644 --- a/gameserver/views/contest.py +++ b/gameserver/views/contest.py @@ -77,7 +77,7 @@ def form_valid(self, form): team = form.cleaned_data["participant"] if ( - self.request.user.in_contest + self.request.in_contest and self.request.user.current_contest.contest == self.object ): contest_participation = models.ContestParticipation.objects.get_or_create( From 33f4e5e74230fd7e021edb93db2232031f2b9d1c Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 12:49:50 -0400 Subject: [PATCH 05/35] Remove creating db object when user has alr submitted. --- gameserver/views/problem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gameserver/views/problem.py b/gameserver/views/problem.py index a648ee4..21502d8 100644 --- a/gameserver/views/problem.py +++ b/gameserver/views/problem.py @@ -169,11 +169,11 @@ def form_valid(self, form): and not self.request.participation.has_solved(self.contest_object) ): messages.success(self.request, "Your flag is correct!") + self._create_submission_object(form, is_correct=True) else: messages.info( self.request, "Your flag is correct, but you have already solved this problem." ) - self._create_submission_object(form, is_correct=True) return super().form_valid(form) def form_invalid(self, form): From e6f6251ddd64d783c2cc57e3b9dce108dc3c470d Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 12:50:58 -0400 Subject: [PATCH 06/35] Added webhook to admin --- gameserver/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gameserver/admin.py b/gameserver/admin.py index c462e04..af8d7c8 100644 --- a/gameserver/admin.py +++ b/gameserver/admin.py @@ -212,6 +212,7 @@ class ContestAdmin(SortableAdminBase, admin.ModelAdmin): "summary", "start_time", "end_time", + "first_blood_webhook", "tags", "max_team_size", "is_public", From 84152fec811c20628a3e8c1368fede0760c259dc Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 12:51:19 -0400 Subject: [PATCH 07/35] fixed naming issues --- gameserver/models/cache.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/gameserver/models/cache.py b/gameserver/models/cache.py index db035f8..7298787 100644 --- a/gameserver/models/cache.py +++ b/gameserver/models/cache.py @@ -241,17 +241,14 @@ def __str__(self) -> str: def update_or_create( cls, change_in_score: int, participant: "ContestParticipation", update_flags: bool = True ): - assert change_in_score > 0 + assert change_in_score > 0, "change_in_score must be greater than 0" queryset = cls.objects.filter(participation=participant) if not queryset.exists(): # no user/team found matching that cls.objects.create( participation=participant, flag_count=int(update_flags), points=change_in_score ) - return cls.update_or_create( - change_in_score=change_in_score, participant=participant, update_flags=update_flags - ) - + with transaction.atomic(): queryset.select_for_update() # prevent race conditions with other team members @@ -265,13 +262,13 @@ def update_or_create( @classmethod def invalidate(cls, participant: "ContestParticipation"): try: - cls.objects.get(participant=participant).delete() + cls.objects.get(participation=participant).delete() except cls.DoesNotExist: pass # participant was not found. @classmethod def get(cls, participant: "ContestParticipation") -> Self | None: - obj = cls.objects.filter(participant=participant) + obj = cls.objects.filter(participation=participant) if obj is None: return None return obj.first() From b1616c26e6eaaabd46d439978958871d406a5443 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 13:19:16 -0400 Subject: [PATCH 08/35] Completed webhook feature. + refactor to singals.py --- gameserver/models/contest.py | 34 ------------------------- gameserver/signals.py | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 34 deletions(-) create mode 100644 gameserver/signals.py diff --git a/gameserver/models/contest.py b/gameserver/models/contest.py index 10b52ba..a7adc33 100644 --- a/gameserver/models/contest.py +++ b/gameserver/models/contest.py @@ -447,37 +447,3 @@ def save(self, *args, **kwargs): ) # see scoreboard.html ContestScore.invalidate(self.participation) super().save(*args, **kwargs) - - -def is_discord(webhook: str): - return webhook.startswith("https://discord.com/api") - - -async def construct_discord_payload(submission: ContestSubmission) -> dict: - return { - 'username': f'{submission.participation.contest.name} First Blood Notifier', - 'avatar_url': Site.objects.get_current().domain + '/static/logo.svg', - 'content': f'First blood on [{submission.problem.problem.name}]({submission.problem.get_absolute_url()} by {submission.participation.participant}!', - } - - -@receiver(post_save, sender=ContestSubmission, dispatch_uid="notify_contest_firstblood") -async def my_handler(sender, instance, created, raw, using, update_fields, **kwargs): - if not created: # only for new submissions - return - if not instance.is_firstblood: - return - - if webhook := instance.participation.contest.first_blood_webhook: - payload: dict = await construct_discord_payload(instance) - if not is_discord(webhook): - payload = payload['content'] - async with aiohttp.ClientSession() as session: - async with session.post( - webhook, - json=payload, - ) as resp: - if resp.status != 200: - text = await resp.text() - logger.error(f"Failed to send webhook: {text} - {resp.status} - {webhook} - {payload}") - diff --git a/gameserver/signals.py b/gameserver/signals.py new file mode 100644 index 0000000..9c66293 --- /dev/null +++ b/gameserver/signals.py @@ -0,0 +1,48 @@ +import logging + +import aiohttp +from asgiref.sync import sync_to_async +from django.contrib.sites.models import Site +from django.db.models.signals import post_save +from django.dispatch import receiver + +from gameserver.models import ContestSubmission + +logger = logging.getLogger(__name__) + + +def is_discord(webhook: str): + return webhook.startswith("https://discord.com/api") + + +def construct_discord_payload(submission: ContestSubmission) -> dict: + BASE_URL = "https://" + Site.objects.get_current().domain + + return { + "username": f"{submission.participation.contest.name} First Blood Notifier", + "avatar_url": BASE_URL + "/static/favicon.png", + "content": f"First blood on [{submission.problem.problem}]({BASE_URL + submission.problem.get_absolute_url()}) by [{submission.participation.participant}]({BASE_URL + submission.participation.participant.get_absolute_url()})!", + } + + +@receiver(post_save, sender=ContestSubmission, dispatch_uid="notify_contest_firstblood") +async def my_handler(sender, instance, created, raw, using, update_fields, **kwargs): + if not created: # only for new submissions + return + if not await instance.ais_firstblooded: + return + + if webhook := instance.participation.contest.first_blood_webhook: + payload: dict = await sync_to_async(construct_discord_payload)(submission=instance) + if not is_discord(webhook): + payload = payload["content"] # only send the content to non-discord webhooks + async with aiohttp.ClientSession() as session: + async with session.post( + webhook, + json=payload, + ) as resp: + if resp.status != 200: + text = await resp.text() + logger.error( + f"Failed to send webhook: {text} - {resp.status} - {webhook} - {payload}" + ) From 2c59af7b2d3fdc2ca3ddd49f8a3f045aefc59e84 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 13:20:02 -0400 Subject: [PATCH 09/35] Optimized imports + fmt --- gameserver/api/routes.py | 2 +- ...re_userscore_alter_user_timezone_and_more.py | 3 ++- ..._options_alter_userscore_options_and_more.py | 1 + ...0035_contest_first_blood_webhook_and_more.py | 3 ++- gameserver/models/cache.py | 2 +- gameserver/models/contest.py | 17 +++++++++++------ gameserver/urls.py | 2 +- gameserver/utils/challenge.py | 1 + gameserver/views/contest.py | 5 +---- 9 files changed, 21 insertions(+), 15 deletions(-) diff --git a/gameserver/api/routes.py b/gameserver/api/routes.py index 82781c0..d62c323 100644 --- a/gameserver/api/routes.py +++ b/gameserver/api/routes.py @@ -1,7 +1,7 @@ import datetime from typing import Any, List -from django.db.models import Case, F, OuterRef, Q, Subquery, When +from django.db.models import F, OuterRef, Subquery from django.shortcuts import get_object_or_404 from ninja import NinjaAPI, Schema diff --git a/gameserver/migrations/0033_contestscore_userscore_alter_user_timezone_and_more.py b/gameserver/migrations/0033_contestscore_userscore_alter_user_timezone_and_more.py index 62fafa2..ace97cc 100644 --- a/gameserver/migrations/0033_contestscore_userscore_alter_user_timezone_and_more.py +++ b/gameserver/migrations/0033_contestscore_userscore_alter_user_timezone_and_more.py @@ -1,8 +1,9 @@ # Generated by Django 4.0.1 on 2024-03-11 01:49 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion + import gameserver.models.profile diff --git a/gameserver/migrations/0034_alter_contestscore_options_alter_userscore_options_and_more.py b/gameserver/migrations/0034_alter_contestscore_options_alter_userscore_options_and_more.py index c4d53da..933e226 100644 --- a/gameserver/migrations/0034_alter_contestscore_options_alter_userscore_options_and_more.py +++ b/gameserver/migrations/0034_alter_contestscore_options_alter_userscore_options_and_more.py @@ -1,6 +1,7 @@ # Generated by Django 5.0.4 on 2024-04-04 13:44 from django.db import migrations, models + import gameserver diff --git a/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py b/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py index 1e39535..6671bb6 100644 --- a/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py +++ b/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py @@ -1,8 +1,9 @@ # Generated by Django 5.0.4 on 2024-04-06 16:06 -import gameserver.models.profile from django.db import migrations, models +import gameserver.models.profile + class Migration(migrations.Migration): dependencies = [ diff --git a/gameserver/models/cache.py b/gameserver/models/cache.py index 7298787..b16bf21 100644 --- a/gameserver/models/cache.py +++ b/gameserver/models/cache.py @@ -248,7 +248,7 @@ def update_or_create( cls.objects.create( participation=participant, flag_count=int(update_flags), points=change_in_score ) - + with transaction.atomic(): queryset.select_for_update() # prevent race conditions with other team members diff --git a/gameserver/models/contest.py b/gameserver/models/contest.py index a7adc33..bb95d7f 100644 --- a/gameserver/models/contest.py +++ b/gameserver/models/contest.py @@ -1,11 +1,8 @@ -import logging from datetime import timedelta -import aiohttp from django.apps import apps from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType -from django.contrib.sites.models import Site from django.core.cache import cache from django.core.cache.utils import make_template_fragment_key from django.core.validators import MinValueValidator @@ -13,8 +10,6 @@ from django.db.models import Count, F, Max, Min, OuterRef, Q, Subquery, Sum from django.db.models.expressions import Window from django.db.models.functions import Coalesce, Rank -from django.db.models.signals import post_save -from django.dispatch import receiver from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property @@ -24,7 +19,6 @@ from ..templatetags.common_tags import strfdelta from . import abstract -logger = logging.getLogger(__name__) # Create your models here. @@ -436,6 +430,17 @@ def is_firstblood(self): return prev_correct_submissions.count() == 1 and prev_correct_submissions.first() == self + @property + async def ais_firstblooded(self): + prev_correct_submissions = ContestSubmission.objects.filter( + problem=self.problem, submission__is_correct=True, pk__lte=self.pk + ) + + return ( + await prev_correct_submissions.acount() == 1 + and await prev_correct_submissions.afirst() == self + ) + def save(self, *args, **kwargs): for key in cache.get(f"contest_ranks_{self.participation.contest.pk}", default=[]): cache.delete(key) diff --git a/gameserver/urls.py b/gameserver/urls.py index 26139c3..23f11d6 100644 --- a/gameserver/urls.py +++ b/gameserver/urls.py @@ -1,4 +1,4 @@ -from django.urls import include, path +from django.urls import path from . import views from .api.routes import api diff --git a/gameserver/utils/challenge.py b/gameserver/utils/challenge.py index b7d2ba7..928e8a0 100644 --- a/gameserver/utils/challenge.py +++ b/gameserver/utils/challenge.py @@ -27,6 +27,7 @@ logger.error("failed to connect to challenge cluster") raise Exception("failed to connect to challenge cluster") + def create_challenge_instance(challenge_spec, problem_id, problem_flag, instance_owner, wait=False): def generate_identifier(): return uuid.uuid4().hex[-7:] diff --git a/gameserver/views/contest.py b/gameserver/views/contest.py index 10156d6..9bf729c 100644 --- a/gameserver/views/contest.py +++ b/gameserver/views/contest.py @@ -76,10 +76,7 @@ def get_form_kwargs(self, *args, **kwargs): def form_valid(self, form): team = form.cleaned_data["participant"] - if ( - self.request.in_contest - and self.request.user.current_contest.contest == self.object - ): + if self.request.in_contest and self.request.user.current_contest.contest == self.object: contest_participation = models.ContestParticipation.objects.get_or_create( team=team, contest=self.object )[0] From 25006552000e87c79b1f001d8f6a1f95aa0e9989 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 13:35:30 -0400 Subject: [PATCH 10/35] removed comment --- gameserver/models/contest.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/gameserver/models/contest.py b/gameserver/models/contest.py index bb95d7f..e3229a2 100644 --- a/gameserver/models/contest.py +++ b/gameserver/models/contest.py @@ -19,9 +19,6 @@ from ..templatetags.common_tags import strfdelta from . import abstract -# Create your models here. - - class ContestTag(abstract.Category): pass From 7fa3004b39310231d5d13e1cee659e625a7eec02 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 13:42:24 -0400 Subject: [PATCH 11/35] disabled lookup when in debug --- gameserver/utils/challenge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gameserver/utils/challenge.py b/gameserver/utils/challenge.py index 928e8a0..5bb77de 100644 --- a/gameserver/utils/challenge.py +++ b/gameserver/utils/challenge.py @@ -23,7 +23,7 @@ api_client = kubernetes.dynamic.DynamicClient(kubernetes.client.ApiClient(cluster_config)) logger.info("connected to challenge cluster") -if api_client is None: +if api_client is None and not settings.DEBUG: logger.error("failed to connect to challenge cluster") raise Exception("failed to connect to challenge cluster") From 153df0e4da0c2a144fe367433b3f8d0402d40b41 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 14:11:01 -0400 Subject: [PATCH 12/35] renamed cache reset method to better reflect it's use. --- gameserver/views/contest.py | 2 +- gameserver/views/user.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/gameserver/views/contest.py b/gameserver/views/contest.py index 9bf729c..186805a 100644 --- a/gameserver/views/contest.py +++ b/gameserver/views/contest.py @@ -197,7 +197,7 @@ def get_title(self): return "Scoreboard for " + self.object.name def get_queryset(self): - if self.model.cache.can_reset(self.request): + if self.model.cache.should_reset(self.request): ContestScore.reset_data(contest=self.object) return ContestScore.ranks(contest=self.object).select_related( "participation__team" diff --git a/gameserver/views/user.py b/gameserver/views/user.py index 89a7f32..526128d 100644 --- a/gameserver/views/user.py +++ b/gameserver/views/user.py @@ -28,10 +28,17 @@ class UserList(ListView, mixin.MetaMixin): paginate_by = 50 title = "Users" - def get_queryset(self): - if self.model.cache.can_reset(self.request): + def get_queryset(self) -> models.QuerySet: + if self.model.cache.should_reset(self.request): UserScore.reset_data() return UserScore.ranks() + + + ranks = UserScore.ranks() + + return ranks + + def get(self, request, *args, **kwargs): if request.in_contest: From dc60d53423debdd44f34039c703f1e4eeeb55e5e Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 15:37:00 -0400 Subject: [PATCH 13/35] Feat/Fix: Users are now ranked by -points -last_correct_submission Added a proper user score admin. --- gameserver/admin.py | 91 ++++++++++- ...35_contest_first_blood_webhook_and_more.py | 46 +++++- gameserver/models/cache.py | 153 ++++++++++++------ gameserver/models/contest.py | 8 + gameserver/models/submission.py | 8 + gameserver/utils/actions.py | 37 +++++ mCTF/settings.py | 25 ++- mCTF/urls.py | 4 +- 8 files changed, 300 insertions(+), 72 deletions(-) create mode 100644 gameserver/utils/actions.py diff --git a/gameserver/admin.py b/gameserver/admin.py index af8d7c8..3d6a057 100644 --- a/gameserver/admin.py +++ b/gameserver/admin.py @@ -3,9 +3,12 @@ from django.contrib.auth import get_user_model from django.contrib.flatpages.admin import FlatPageAdmin from django.contrib.flatpages.models import FlatPage +from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from . import models +from .models import Submission, ContestSubmission +from .utils.actions import * User = get_user_model() @@ -226,6 +229,7 @@ class ContestAdmin(SortableAdminBase, admin.ModelAdmin): "start_time", "end_time", ] + actions = [recalculate_score] def has_view_permission(self, request, obj=None): if request.user.has_perm("gameserver.view_contest"): @@ -278,11 +282,93 @@ class UserAdmin(admin.ModelAdmin): "username", "full_name", ] + actions = [recalculate_user_scores, recalculate_all_user_scores] + + +class UserScoreAdmin(admin.ModelAdmin): + fields = ( + "user", + "points", + "flag_count", + "last_correct_submission", + "last_correct_submission_obj", + ) + list_display = [ + "user", + "points", + "last_correct_submission", + ] + ordering = ["-points"] + search_fields = [ + "user__username", + "user__full_name", + ] + readonly_fields = [ + "user", + "points", + "flag_count", + "last_correct_submission", + "last_correct_submission_obj", + ] + + def last_correct_submission_obj(self, obj): + try: + obj = Submission.objects.filter( + user=obj.user, is_correct=True, problem__is_public=True + ).latest("date_created") + except Submission.DoesNotExist: + return "No correct submissions" + print(format_html("{url}", url=obj.get_absolute_admin_url())) + return format_html("{url}", url=obj.get_absolute_admin_url()) + + last_correct_submission_obj.allow_tags = True + + last_correct_submission_obj.short_description = "Last correct submission URL" + + +class ContestScoreAdmin(admin.ModelAdmin): + fields = ( + "participation", + "points", + "flag_count", + "last_correct_submission", + "last_correct_submission_obj", + ) + list_display = [ + "participation", + "points", + "last_correct_submission", + ] + list_filter = ["participation__contest"] + ordering = ["-points"] + search_fields = [ + "participation__team__name", + "participation__team__members__username", + ] + readonly_fields = [ + "participation", + "points", + "flag_count", + "last_correct_submission", + "last_correct_submission_obj", + ] + + def last_correct_submission_obj(self, obj): + try: + obj = ContestSubmission.objects.filter( + participation=obj.participation, submission__is_correct=True + ).latest("submission__date_created") + except ContestSubmission.DoesNotExist: + return "No correct submissions" + return format_html("{url}", url=obj.get_absolute_admin_url()) + + last_correct_submission_obj.allow_tags = True + last_correct_submission_obj.short_description = "Last correct submission URL" admin.site.register(User, UserAdmin) -admin.site.register(models.ContestScore) -admin.site.register(models.UserScore) +admin.site.register(models.ContestScore, ContestScoreAdmin) +admin.site.register(models.UserScore, UserScoreAdmin) admin.site.register(models.Problem, ProblemAdmin) admin.site.register(models.Submission, SubmissionAdmin) admin.site.register(models.ProblemType) @@ -296,6 +382,7 @@ class UserAdmin(admin.ModelAdmin): admin.site.register(models.Contest, ContestAdmin) admin.site.register(models.ContestTag) admin.site.register(models.ContestParticipation) +admin.site.register(models.ContestSubmission) admin.site.site_header = "mCTF administration" admin.site.site_title = "mCTF admin" diff --git a/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py b/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py index 6671bb6..0a8e34f 100644 --- a/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py +++ b/gameserver/migrations/0035_contest_first_blood_webhook_and_more.py @@ -1,8 +1,10 @@ -# Generated by Django 5.0.4 on 2024-04-06 16:06 - -from django.db import migrations, models +# Generated by Django 5.0.4 on 2024-04-06 18:37 +import datetime +import django.db.models.deletion import gameserver.models.profile +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): @@ -19,6 +21,26 @@ class Migration(migrations.Migration): help_text="URL to send a POST request to when a user gets the first blood on a problem", ), ), + migrations.AddField( + model_name="contestscore", + name="last_correct_submission", + field=models.DateTimeField( + blank=True, + default=datetime.datetime(1971, 1, 1, 0, 0), + editable=False, + help_text="The date of the last correct submission.", + ), + ), + migrations.AddField( + model_name="userscore", + name="last_correct_submission", + field=models.DateTimeField( + blank=True, + default=datetime.datetime(1971, 1, 1, 0, 0), + editable=False, + help_text="The date of the last correct submission.", + ), + ), migrations.AlterField( model_name="contest", name="organizations", @@ -29,6 +51,15 @@ class Migration(migrations.Migration): to="gameserver.organization", ), ), + migrations.AlterField( + model_name="contestscore", + name="participation", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="score_cache", + to="gameserver.contestparticipation", + ), + ), migrations.AlterField( model_name="user", name="timezone", @@ -636,4 +667,13 @@ class Migration(migrations.Migration): max_length=50, ), ), + migrations.AlterField( + model_name="userscore", + name="user", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="score_cache", + to=settings.AUTH_USER_MODEL, + ), + ), ] diff --git a/gameserver/models/cache.py b/gameserver/models/cache.py index b16bf21..5cc5507 100644 --- a/gameserver/models/cache.py +++ b/gameserver/models/cache.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import TYPE_CHECKING, Callable, Optional, Protocol, Self from django.apps import apps @@ -7,21 +8,22 @@ Case, Count, F, - OuterRef, QuerySet, - Subquery, Sum, Value, When, Window, ) -from django.db.models.functions import Coalesce, Rank, RowNumber +from django.db.models.functions import Coalesce, Rank from django.http import HttpRequest +from django.utils import timezone if TYPE_CHECKING: from .contest import Contest, ContestParticipation, ContestSubmission from .profile import User +EPOCH_TIME = datetime(1971, 1, 1, 0, 0, 0) + class ResetableCache(Protocol): def can_reset(cls, request: HttpRequest) -> None: @@ -50,14 +52,23 @@ def _can_reset(cls, request: HttpRequest): class UserScore(CacheMeta): - user = models.OneToOneField("User", on_delete=models.CASCADE, db_index=True) + user = models.OneToOneField( + "User", related_name="score_cache", on_delete=models.CASCADE, db_index=True + ) points = models.PositiveIntegerField(help_text="The amount of points.", default=0) flag_count = models.PositiveIntegerField( help_text="The amount of flags the user/team has.", default=0 ) + last_correct_submission = models.DateTimeField( + help_text="The date of the last correct submission.", + # auto_now=True, auto now is not used as it does not allow you to override the value + editable=False, + blank=True, + default=EPOCH_TIME, # only used for migration (overwritten by reset_score) + ) @classmethod - def can_reset(cls, request: HttpRequest): + def should_reset(cls, request: HttpRequest): return cls._can_reset(request) and request.user.has_perm( "gameserver.can_reset_cache_user_score" ) @@ -73,9 +84,9 @@ def ranks(cls): qs = cls.objects.annotate( rank=Window( expression=Rank(), - order_by=F("points").desc(), + order_by=("-points", "-last_correct_submission"), ) - ).order_by("rank", "flag_count") + ).order_by("rank") return qs @classmethod @@ -84,15 +95,26 @@ def update_or_create(cls, change_in_score: int, user: "User", update_flags: bool queryset = cls.objects.filter(user=user) if not queryset.exists(): # no user found matching that - cls.objects.create(user=user, flag_count=int(update_flags), points=change_in_score) + cls.objects.create( + user=user, + flag_count=int(update_flags), + points=change_in_score, + last_correct_submission=timezone.now(), + ) return cls.update_or_create( change_in_score=change_in_score, user=user, update_flags=update_flags ) if update_flags: - queryset.update(points=F("points") + change_in_score) + queryset.update( + points=F("points") + change_in_score, + flag_count=F("flag_count") + 1, + last_correct_submission=timezone.now(), + ) else: - queryset.update(points=F("points") + change_in_score, flag_count=F("flag_count") + 1) + queryset.update( + points=F("points") + change_in_score, last_correct_submission=timezone.now() + ) @classmethod def invalidate(cls, user: "User"): @@ -109,46 +131,62 @@ def get(cls, user: "User") -> Self | None: return obj.first() @classmethod - def reset_data(cls): + def reset_data(cls, users: Optional[QuerySet["User"]] = None): from django.contrib.auth import get_user_model + from gameserver.models import Submission + + if users is None: + users = get_user_model().objects.all() + cls.objects.all().delete() # clear past objs + else: + cls.objects.filter(user__in=users).delete() # clear past objs - cls.objects.all().delete() # clear inital objs - UserModel = get_user_model() - users = UserModel.objects.all() scores_to_create = [] + for user in users: queryset = user._get_unique_correct_submissions() queryset = queryset.aggregate( points=Coalesce(Sum("problem__points"), 0), flags=Count("problem"), ) + try: + last_correct_submission = ( + Submission.objects.filter(user=user, problem__is_public=True, is_correct=True) + .latest("date_created") + .date_created + ) + except Submission.DoesNotExist: # user have never submitted to a public problem + last_correct_submission = EPOCH_TIME + scores_to_create.append( - UserScore(user=user, flag_count=queryset["flags"], points=queryset["points"]) + UserScore( + user=user, + flag_count=queryset["flags"], + points=queryset["points"], + last_correct_submission=last_correct_submission, + ) ) # Use bulk_create to create all objects in a single query cls.objects.bulk_create(scores_to_create) -def get_contest_submission() -> Callable[[], "ContestSubmission"]: - model: "ContestSubmission" = None - - def inner(): - nonlocal model - if model is None: - model = apps.get_model("gameserver", "ContestSubmission", require_ready=True) - return model - - return inner - - class ContestScore(CacheMeta): participation = models.OneToOneField( - "ContestParticipation", on_delete=models.CASCADE, db_index=True + "ContestParticipation", related_name="score_cache", on_delete=models.CASCADE, db_index=True ) points = models.PositiveIntegerField(help_text="The amount of points.", default=0) flag_count = models.PositiveIntegerField( help_text="The amount of flags the user/team has.", default=0 ) + + last_correct_submission = models.DateTimeField( + help_text="The date of the last correct submission.", + # auto_now=True, auto now is not used as it does not allow you to override the value + editable=False, + blank=True, + default=EPOCH_TIME, # only used for migration (overwritten by reset_score) + ) + # contest_id = models.IntegerField( # help_text="The id for which contest this applies to. Do not change this value manually.", # blank=True, @@ -160,7 +198,7 @@ class ContestScore(CacheMeta): # super().save(*args, **kwargs) @classmethod - def can_reset(cls, request: HttpRequest): + def should_reset(cls, request: HttpRequest): return cls._can_reset(request) and request.user.has_perm( "gameserver.can_reset_cache_user_score" ) @@ -189,12 +227,6 @@ def ranks( # perm_edit_all_contests = Permission.objects.get( # codename="edit_all_contests", content_type=contest_content_type # ) - max_submission_time_subquery = ( - get_contest_submission()() - .objects.filter(participation=OuterRef("participation")) - .order_by("-submission__date_created") - .values("submission__date_created")[:1] - ) if participation: if isinstance(participation, QuerySet): query = cls.objects.filter(participation__in=participation) @@ -218,16 +250,11 @@ def ranks( default=Value(True), output_field=BooleanField(), ), - sub_rank=Window( - expression=Rank(), - order_by=F("points").desc(), - ), rank=Window( - expression=RowNumber(), - order_by=F("points").desc(), + expression=Rank(), + order_by=("-points", "-last_correct_submission"), ), - max_submission_time=Subquery(max_submission_time_subquery), - ).order_by("rank", "flag_count", "-max_submission_time") + ).order_by("rank") return data def __str__(self) -> str: @@ -246,17 +273,24 @@ def update_or_create( if not queryset.exists(): # no user/team found matching that cls.objects.create( - participation=participant, flag_count=int(update_flags), points=change_in_score + participation=participant, + flag_count=int(update_flags), + points=change_in_score, + last_correct_submission=timezone.now(), ) with transaction.atomic(): queryset.select_for_update() # prevent race conditions with other team members if update_flags: - queryset.update(points=F("points") + change_in_score) + queryset.update( + points=F("points") + change_in_score, + flag_count=F("flag_count") + 1, + last_correct_submission=timezone.now(), + ) else: queryset.update( - points=F("points") + change_in_score, flag_count=F("flag_count") + 1 + points=F("points") + change_in_score, last_correct_submission=timezone.now() ) @classmethod @@ -266,17 +300,13 @@ def invalidate(cls, participant: "ContestParticipation"): except cls.DoesNotExist: pass # participant was not found. - @classmethod - def get(cls, participant: "ContestParticipation") -> Self | None: - obj = cls.objects.filter(participation=participant) - if obj is None: - return None - return obj.first() - @classmethod def reset_data(cls, contest: Optional["Contest"] = None, all: bool = False): assert contest is not None or all, "Either contest or all must be set to True" ContestModel = apps.get_model("gameserver", "Contest") + ContestSubmissionModel: ContestSubmission = apps.get_model( + "gameserver", "ContestSubmission" + ) if all: contests = ContestModel.objects.all() for contest in contests: @@ -292,11 +322,28 @@ def reset_data(cls, contest: Optional["Contest"] = None, all: bool = False): points=Coalesce(Sum("problem__points"), 0), flags=Count("problem"), ) + try: + last_correct_submission = ( + ContestSubmissionModel.objects.filter( + problem__contest=contest, + participation=participant, + problem__problem__is_public=True, + submission__is_correct=True, + ) + .prefetch_related("submission") + .latest("submission__date_created") + .submission.date_created + ) + except ( + ContestSubmissionModel.DoesNotExist + ): # user have never submitted to a public problem + last_correct_submission = EPOCH_TIME scores_to_create.append( ContestScore( participation=participant, flag_count=queryset["flags"], points=queryset["points"], + last_correct_submission=last_correct_submission, ) ) # Use bulk_create to create all objects in a single query diff --git a/gameserver/models/contest.py b/gameserver/models/contest.py index e3229a2..72374a3 100644 --- a/gameserver/models/contest.py +++ b/gameserver/models/contest.py @@ -13,12 +13,14 @@ from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property +from django.utils.html import format_html from gameserver.models.cache import ContestScore from ..templatetags.common_tags import strfdelta from . import abstract + class ContestTag(abstract.Category): pass @@ -415,6 +417,12 @@ class ContestSubmission(models.Model): def __str__(self): return f"{self.participation.participant}'s submission for {self.problem.problem.name} in {self.problem.contest.name}" + def get_absolute_admin_url(self): + url = reverse( + "admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name), args=[self.id] + ) + return format_html('{url}', url=url) + @cached_property def is_correct(self): return self.submission.is_correct diff --git a/gameserver/models/submission.py b/gameserver/models/submission.py index 5b2ecb5..70ae3c6 100644 --- a/gameserver/models/submission.py +++ b/gameserver/models/submission.py @@ -1,6 +1,8 @@ from django.db import models from django.db.models import Q +from django.urls import reverse from django.utils.functional import cached_property +from django.utils.html import format_html from .cache import UserScore @@ -24,6 +26,12 @@ class Submission(models.Model): date_created = models.DateTimeField(auto_now_add=True) content = models.CharField(max_length=256, null=True, default=None) + def get_absolute_admin_url(self): + url = reverse( + "admin:%s_%s_change" % (self._meta.app_label, self._meta.model_name), args=[self.id] + ) + return format_html('{url}', url=url) + def __str__(self): return f"{self.user.username}'s submission for {self.problem.name}" diff --git a/gameserver/utils/actions.py b/gameserver/utils/actions.py new file mode 100644 index 0000000..051da1c --- /dev/null +++ b/gameserver/utils/actions.py @@ -0,0 +1,37 @@ +import time + +from django.contrib import messages + +from gameserver.models import ContestScore, UserScore + + +def recalculate_score(self, request, queryset): + start = time.monotonic_ns() + for contest in queryset: + ContestScore.reset_data(contest) + + end = time.monotonic_ns() + messages.success(request, f"Time taken: {(end - start) / 1e9} seconds") + + +recalculate_score.short_description = "Recalculate scores for selected contests." + + +def recalculate_user_scores(self, request, queryset): + start = time.monotonic_ns() + UserScore.reset_data(users=queryset) + end = time.monotonic_ns() + messages.success(request, f"Time taken: {(end - start) / 1e9} seconds") + + +recalculate_user_scores.short_description = "Recalculate scores for selected users." + + +def recalculate_all_user_scores(self, request, queryset): + start = time.monotonic_ns() + UserScore.reset_data() + end = time.monotonic_ns() + messages.success(request, f"Time taken: {(end - start) / 1e9} seconds") + + +recalculate_all_user_scores.short_description = "Recalculate scores for all users. NOTE: You must select at least one user to run this action due to Django admin limitations." diff --git a/mCTF/settings.py b/mCTF/settings.py index c2e6051..2b2ab04 100644 --- a/mCTF/settings.py +++ b/mCTF/settings.py @@ -63,14 +63,14 @@ "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware", "gameserver.middleware.RedirectFallbackTemporaryMiddleware", "allauth.account.middleware.AccountMiddleware", - 'gameserver.middleware.ErrorLogMiddleware', + "gameserver.middleware.ErrorLogMiddleware", # ↑ keep last to log errors from middlewares ] CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'CTFx', + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": "CTFx", } } @@ -300,9 +300,7 @@ DEFAULT_TIMEZONE = "UTC" -MISTUNE_PLUGINS = ( - "strikethrough", -) +MISTUNE_PLUGINS = ("strikethrough",) try: from mCTF.config import * @@ -315,10 +313,11 @@ if STORAGES.get("staticfiles") == None: raise TypeError("STORAGES[staticfiles] must not be blank") -INSTALLED_APPS += ["debug_toolbar"] +if DEBUG: + INSTALLED_APPS += ["debug_toolbar"] -MIDDLEWARE += [ - # ... - "debug_toolbar.middleware.DebugToolbarMiddleware", - # ... -] + MIDDLEWARE += [ + # ... + "debug_toolbar.middleware.DebugToolbarMiddleware", + # ... + ] diff --git a/mCTF/urls.py b/mCTF/urls.py index e45c48f..38dff54 100644 --- a/mCTF/urls.py +++ b/mCTF/urls.py @@ -13,14 +13,16 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from django.conf import settings from django.contrib import admin from django.contrib.flatpages import views as flatpage_views from django.urls import include, path urlpatterns = [ - path("__debug__/", include("debug_toolbar.urls")), path("admin/", admin.site.urls), path("", include("gameserver.urls")), path("accounts/", include("allauth.urls")), path("/", flatpage_views.flatpage), ] +if settings.DEBUG: + urlpatterns += path("__debug__/", include("debug_toolbar.urls")), From 0d3102163703fa2e00da3a991757d2a7657c72f8 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 15:39:09 -0400 Subject: [PATCH 14/35] replace == with is (pythonic!!!!!) --- mCTF/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mCTF/settings.py b/mCTF/settings.py index 2b2ab04..d5b6ec8 100644 --- a/mCTF/settings.py +++ b/mCTF/settings.py @@ -308,9 +308,9 @@ raise TypeError("Please create a config file to override values in config.py") -if STORAGES.get("default") == None: +if STORAGES.get("default") is None: raise TypeError("STORAGES[default] must not be blank") -if STORAGES.get("staticfiles") == None: +if STORAGES.get("staticfiles") is None: raise TypeError("STORAGES[staticfiles] must not be blank") if DEBUG: From f273841b4d8d2046f46d1be74b38fa5debe84315 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 15:39:41 -0400 Subject: [PATCH 15/35] remove duplicated AUTHENTICATION_BACKENDS def --- mCTF/settings.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mCTF/settings.py b/mCTF/settings.py index d5b6ec8..3cbc26f 100644 --- a/mCTF/settings.py +++ b/mCTF/settings.py @@ -180,11 +180,6 @@ ACCOUNT_EMAIL_VERIFICATION = "optional" ACCOUNT_SIGNUP_REDIRECT_URL = "/" -AUTHENTICATION_BACKENDS = ( - "django.contrib.auth.backends.ModelBackend", - "allauth.account.auth_backends.AuthenticationBackend", -) - ACCOUNT_FORMS = {"signup": "gameserver.forms.MCTFSignupForm"} TOS_URL = "/tos" From 8c579ef46145147ef5213977f61aeffabd54b4f9 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 15:57:22 -0400 Subject: [PATCH 16/35] Describe issues with implementing fetching the rank of a single user. --- gameserver/models/cache.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/gameserver/models/cache.py b/gameserver/models/cache.py index 5cc5507..7a50894 100644 --- a/gameserver/models/cache.py +++ b/gameserver/models/cache.py @@ -124,12 +124,17 @@ def invalidate(cls, user: "User"): pass # user was not found. @classmethod - def get(cls, user: "User") -> Self | None: - obj = cls.objects.filter(user=user) - if obj is None: - return None - return obj.first() - + def get_rank(cls, user: "User") -> int: + """ + Get the rank of a user + + There are a couple issues with implementing this function as + cls.ranks().get(user=user).rank + - The biggest is django is lazy and the user's rank will always be 1 + The only way I see to implement this would be to use raw SQL (see cls.ranks().query) + """ + raise NotImplementedError + @classmethod def reset_data(cls, users: Optional[QuerySet["User"]] = None): from django.contrib.auth import get_user_model From 7e31cfc9242e54889872efb6a80f8f70bb215889 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 16:00:36 -0400 Subject: [PATCH 17/35] remove unused/useless code. --- gameserver/models/cache.py | 15 --------- gameserver/models/contest.py | 64 ------------------------------------ gameserver/models/profile.py | 15 ++------- 3 files changed, 2 insertions(+), 92 deletions(-) diff --git a/gameserver/models/cache.py b/gameserver/models/cache.py index 7a50894..0a38599 100644 --- a/gameserver/models/cache.py +++ b/gameserver/models/cache.py @@ -116,13 +116,6 @@ def update_or_create(cls, change_in_score: int, user: "User", update_flags: bool points=F("points") + change_in_score, last_correct_submission=timezone.now() ) - @classmethod - def invalidate(cls, user: "User"): - try: - cls.objects.get(user=user).delete() - except cls.DoesNotExist: - pass # user was not found. - @classmethod def get_rank(cls, user: "User") -> int: """ @@ -297,14 +290,6 @@ def update_or_create( queryset.update( points=F("points") + change_in_score, last_correct_submission=timezone.now() ) - - @classmethod - def invalidate(cls, participant: "ContestParticipation"): - try: - cls.objects.get(participation=participant).delete() - except cls.DoesNotExist: - pass # participant was not found. - @classmethod def reset_data(cls, contest: Optional["Contest"] = None, all: bool = False): assert contest is not None or all, "Either contest or all must be set to True" diff --git a/gameserver/models/contest.py b/gameserver/models/contest.py index 72374a3..c20af6d 100644 --- a/gameserver/models/contest.py +++ b/gameserver/models/contest.py @@ -102,70 +102,9 @@ def has_problem(self, problem): else: return self.problems.filter(problem__pk=problem.pk).exists() - @cached_property - def __meta_key(self): - return f"contest_ranks_{self.pk}" - def ranks(self): return self.ContestScore.ranks(self) - def _ranks(self, queryset=None): - if queryset is None: - contest_content_type = ContentType.objects.get_for_model(Contest) - perm_edit_all_contests = Permission.objects.get( - codename="edit_all_contests", content_type=contest_content_type - ) - - queryset = self.participations.exclude( - Q(participants__is_superuser=True) - | Q(participants__groups__permissions=perm_edit_all_contests) - | Q(participants__user_permissions=perm_edit_all_contests) - | Q(participants__in=self.organizers.all()) - | Q(participants__in=self.curators.all()) - ) - - submissions_with_points = ( - ContestSubmission.objects.filter( - participation=OuterRef("pk"), submission__is_correct=True - ) - .order_by() - .values("submission__problem") - .distinct() - .annotate(sub_pk=Min("pk")) - .values("sub_pk") - ) - return ( - queryset.annotate( - points=Coalesce( - Sum( - "submission__problem__points", - filter=Q(submission__in=Subquery(submissions_with_points)), - ), - 0, - ), - flags=Coalesce( - Count( - "submission__pk", filter=Q(submission__in=Subquery(submissions_with_points)) - ), - 0, - ), - most_recent_solve_time=Coalesce( - Max( - "submission__submission__date_created", - filter=Q(submission__in=Subquery(submissions_with_points)), - ), - self.start_time, - ), - ) - .annotate( - rank=Window( - expression=Rank(), - order_by=[F("points").desc(), F("most_recent_solve_time").asc()], - ) - ) - .order_by("rank", "flags") - ) - def is_visible_by(self, user): if self.is_public: return True @@ -447,13 +386,10 @@ async def ais_firstblooded(self): ) def save(self, *args, **kwargs): - for key in cache.get(f"contest_ranks_{self.participation.contest.pk}", default=[]): - cache.delete(key) cache.delete( make_template_fragment_key("participant_data", [self.participation]) ) # see participation.html cache.delete( make_template_fragment_key("user_participation", [self.participation]) ) # see scoreboard.html - ContestScore.invalidate(self.participation) super().save(*args, **kwargs) diff --git a/gameserver/models/profile.py b/gameserver/models/profile.py index 9b380ab..e62cba7 100644 --- a/gameserver/models/profile.py +++ b/gameserver/models/profile.py @@ -62,28 +62,17 @@ def _get_unique_correct_submissions(self, queryset=None): return queryset.values("problem", "problem__points").distinct() def points(self, queryset=None): - cache = UserScore.get(user=self) - + cache = UserScore.objects.filter(user=self).first() if cache is None: return 0 return cache.points def flags(self, queryset=None): - cache = UserScore.get(user=self) + cache = UserScore.objects.filter(user=self).first() if cache is None: return 0 return cache.flag_count - def rank(self, queryset=None): - return ( - self.cached_ranks(f"user_{self.pk}", queryset) - .filter( - points__gt=self.points, - ) - .count() - + 1 - ) - @classmethod def ranks(cls, queryset=None): if queryset is None: From c92e9c005a6189493d379e6f75178f4bbde9be3e Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 16:15:16 -0400 Subject: [PATCH 18/35] Added user list and contest lb filtering (without templates) --- gameserver/models/cache.py | 5 +++-- gameserver/views/contest.py | 13 ++++++++++++- gameserver/views/user.py | 12 ++++++------ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/gameserver/models/cache.py b/gameserver/models/cache.py index 0a38599..833e02a 100644 --- a/gameserver/models/cache.py +++ b/gameserver/models/cache.py @@ -120,14 +120,14 @@ def update_or_create(cls, change_in_score: int, user: "User", update_flags: bool def get_rank(cls, user: "User") -> int: """ Get the rank of a user - + There are a couple issues with implementing this function as cls.ranks().get(user=user).rank - The biggest is django is lazy and the user's rank will always be 1 The only way I see to implement this would be to use raw SQL (see cls.ranks().query) """ raise NotImplementedError - + @classmethod def reset_data(cls, users: Optional[QuerySet["User"]] = None): from django.contrib.auth import get_user_model @@ -290,6 +290,7 @@ def update_or_create( queryset.update( points=F("points") + change_in_score, last_correct_submission=timezone.now() ) + @classmethod def reset_data(cls, contest: Optional["Contest"] = None, all: bool = False): assert contest is not None or all, "Either contest or all must be set to True" diff --git a/gameserver/views/contest.py b/gameserver/views/contest.py index 186805a..a3f0cb6 100644 --- a/gameserver/views/contest.py +++ b/gameserver/views/contest.py @@ -199,10 +199,21 @@ def get_title(self): def get_queryset(self): if self.model.cache.should_reset(self.request): ContestScore.reset_data(contest=self.object) - return ContestScore.ranks(contest=self.object).select_related( + + ranks = ContestScore.ranks(contest=self.object).select_related( "participation__team" ) # select related reduces queries from around 54 to 17ish so 8ms to 5ms + query = self.request.GET.get("q") + + if query: + ranks = ranks.filter( + Q(participation__team__name__icontains=query) + | Q(participation__team__members__username__icontains=query) + | Q(participation__team__members__full_name__icontains=query) + ).distinct() + return ranks + def _get_contest(self, slug): return get_object_or_404(models.Contest, slug=slug) diff --git a/gameserver/views/user.py b/gameserver/views/user.py index 526128d..bd58e87 100644 --- a/gameserver/views/user.py +++ b/gameserver/views/user.py @@ -31,14 +31,14 @@ class UserList(ListView, mixin.MetaMixin): def get_queryset(self) -> models.QuerySet: if self.model.cache.should_reset(self.request): UserScore.reset_data() - return UserScore.ranks() - - + + query = self.request.GET.get("q") + ranks = UserScore.ranks() - - return ranks + if query: + ranks = ranks.filter(user__username__icontains=query) - + return ranks def get(self, request, *args, **kwargs): if request.in_contest: From 097b720306cec7c45c91b02dc414f29b855be25b Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 16:53:38 -0400 Subject: [PATCH 19/35] Added templates for searching. --- gameserver/templatetags/common_tags.py | 7 ++++++- gameserver/templatetags/search.py | 19 +++++++++++++++++++ templates/contest/scoreboard.html | 3 +++ templates/snippets/search.html | 12 ++++++++++++ templates/table-list.html | 2 ++ templates/user/list.html | 4 ++++ 6 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 gameserver/templatetags/search.py create mode 100644 templates/snippets/search.html diff --git a/gameserver/templatetags/common_tags.py b/gameserver/templatetags/common_tags.py index 7f1e498..8c2b8b3 100644 --- a/gameserver/templatetags/common_tags.py +++ b/gameserver/templatetags/common_tags.py @@ -9,11 +9,16 @@ def debug(): return settings.DEBUG -@register.filter +@register.simple_tag def startswith(string, substring): return string.startswith(substring) +@register.simple_tag +def endswith(string, substring): + return string.endswith(substring) + + @register.filter def split(string, split_char=" "): return string.split(split_char) diff --git a/gameserver/templatetags/search.py b/gameserver/templatetags/search.py new file mode 100644 index 0000000..3fc6cb1 --- /dev/null +++ b/gameserver/templatetags/search.py @@ -0,0 +1,19 @@ +import re + +from django import template +from django.shortcuts import resolve_url + +register = template.Library() + + +@register.filter +def get_search_page(request): + if request.path in ["/users/", "/users"]: + return resolve_url("user_list") + elif match := re.fullmatch(r"^/contest/([^/]+)/scoreboard/organization/([^/]+$)", request.path): + return resolve_url( + "contest_organization_scoreboard", contest_slug=match.group(1), org_slug=match.group(2) + ) + elif match := re.fullmatch(r"^/contest/([^/]+)/scoreboard$", request.path): + return resolve_url("contest_scoreboard", slug=match.group(1)) + return diff --git a/templates/contest/scoreboard.html b/templates/contest/scoreboard.html index aafc329..a305e2c 100644 --- a/templates/contest/scoreboard.html +++ b/templates/contest/scoreboard.html @@ -5,6 +5,9 @@ {% block heading %}{% if org %}{{ org.short_name }} {% endif %}Scoreboard for {{ contest }}{% endblock %} +{% block top %} + {% include 'snippets/search.html' %} +{% endblock top %} {% block th %} Rank Name diff --git a/templates/snippets/search.html b/templates/snippets/search.html new file mode 100644 index 0000000..d8e809e --- /dev/null +++ b/templates/snippets/search.html @@ -0,0 +1,12 @@ +{% load search %} + +
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/templates/table-list.html b/templates/table-list.html index 8d9b5ac..558703e 100644 --- a/templates/table-list.html +++ b/templates/table-list.html @@ -2,6 +2,8 @@ {% block content %}
+ {% block top %} + {% endblock %}
{% block before_table %} {% endblock %} diff --git a/templates/user/list.html b/templates/user/list.html index 1c455cf..9911573 100644 --- a/templates/user/list.html +++ b/templates/user/list.html @@ -1,5 +1,9 @@ {% extends 'table-list.html' %} +{% block top %} +{% include 'snippets/search.html' %} +{% endblock top %} + {% block th %} Rank Username From bd866e1ffb96f23f759097161ff6971f08b83065 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 6 Apr 2024 16:55:36 -0400 Subject: [PATCH 20/35] Added disclaimer --- templates/snippets/search.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/snippets/search.html b/templates/snippets/search.html index d8e809e..57a6092 100644 --- a/templates/snippets/search.html +++ b/templates/snippets/search.html @@ -2,7 +2,7 @@
- +