From d62e9e3d271561ed305f8d2a364b927a0ff344be Mon Sep 17 00:00:00 2001
From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com>
Date: Sun, 8 Dec 2019 14:08:19 +0530
Subject: [PATCH 01/34] Worker: Add tests for
load_challenge_and_return_max_submissions in worker(#2497)
* Added test for load_challenge_and_return_max_submissions
* Added test for submission worker [`load_challenge_and_return_max_submissions`](https://github.com/Cloud-CV/EvalAI/blob/master/scripts/workers/submission_worker.py#L648)
* According to [GCI-task](https://codein.withgoogle.com/dashboard/task-instances/5189522401263616/)
* Update test_submission_worker.py
* Use mock
* Removed attempt to access database which caused Travis build to fail.
* Fix Travis CI errors
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Revert "Update test_submission_worker.py"
This reverts commit c602515ac0479f97b4f8427bfbafec926d114a20.
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
---
tests/unit/worker/test_submission_worker.py | 54 ++++++++++++++++++++-
1 file changed, 52 insertions(+), 2 deletions(-)
diff --git a/tests/unit/worker/test_submission_worker.py b/tests/unit/worker/test_submission_worker.py
index 05ebf3615b..7b5597b033 100644
--- a/tests/unit/worker/test_submission_worker.py
+++ b/tests/unit/worker/test_submission_worker.py
@@ -1,21 +1,31 @@
import boto3
+import mock
import os
import shutil
import tempfile
+from datetime import timedelta
from moto import mock_sqs
from os.path import join
-from unittest import TestCase
+from django.contrib.auth.models import User
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.utils import timezone
+
+from rest_framework.test import APITestCase
+
+from challenges.models import Challenge
+from hosts.models import ChallengeHostTeam
from scripts.workers.submission_worker import (
create_dir,
create_dir_as_python_package,
+ load_challenge_and_return_max_submissions,
return_file_url_per_environment,
get_or_create_sqs_queue
)
-class BaseAPITestClass(TestCase):
+class BaseAPITestClass(APITestCase):
def setUp(self):
self.BASE_TEMP_DIR = tempfile.mkdtemp()
self.temp_directory = join(self.BASE_TEMP_DIR, "temp_dir")
@@ -30,6 +40,32 @@ def setUp(self):
aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
)
+ self.user = User.objects.create(
+ username="someuser",
+ email="user@test.com",
+ password="secret_password",
+ )
+ self.challenge_host_team = ChallengeHostTeam.objects.create(
+ team_name="Test Challenge Host Team", created_by=self.user
+ )
+ self.challenge = Challenge.objects.create(
+ title="Test Challenge",
+ description="Description for test challenge",
+ terms_and_conditions="Terms and conditions for test challenge",
+ submission_guidelines="Submission guidelines for test challenge",
+ creator=self.challenge_host_team,
+ start_date=timezone.now() - timedelta(days=2),
+ end_date=timezone.now() + timedelta(days=1),
+ published=False,
+ enable_forum=True,
+ anonymous_leaderboard=False,
+ max_concurrent_submission_evaluation=100,
+ evaluation_script=SimpleUploadedFile(
+ "test_sample_file.txt",
+ b"Dummy file content",
+ content_type="text/plain",
+ ),
+ )
def test_create_dir(self):
create_dir(self.temp_directory)
@@ -45,6 +81,20 @@ def test_return_file_url_per_environment(self):
returned_url = return_file_url_per_environment(self.url)
self.assertEqual(returned_url, "http://testserver/test/url")
+ @mock.patch("scripts.workers.submission_worker.load_challenge")
+ def test_load_challenge_and_return_max_submissions(self, mocked_load_challenge):
+ q_params = {"pk": self.challenge.pk}
+ response = load_challenge_and_return_max_submissions(q_params)
+ mocked_load_challenge.assert_called_with(self.challenge)
+ self.assertEqual(response, (self.challenge.max_concurrent_submission_evaluation, self.challenge))
+
+ @mock.patch("scripts.workers.submission_worker.logger.exception")
+ def test_load_challenge_and_return_max_submissions_when_challenge_does_not_exist(self, mock_logger):
+ non_existing_challenge_pk = self.challenge.pk + 1
+ with self.assertRaises(Challenge.DoesNotExist):
+ load_challenge_and_return_max_submissions({"pk": non_existing_challenge_pk})
+ mock_logger.assert_called_with("Challenge with pk {} doesn't exist".format(non_existing_challenge_pk))
+
@mock_sqs()
def test_get_or_create_sqs_queue_for_existing_queue(self):
self.sqs_client.create_queue(QueueName="test_queue")
From 0f52da4654040baf77d98938d0d4684783884d01 Mon Sep 17 00:00:00 2001
From: Sanjeev Singh
Date: Mon, 16 Dec 2019 03:05:50 +0530
Subject: [PATCH 02/34] Backend: Fix sending participant invite to a user if
user's email is in blocked/banned emails(#2493)
* Add check to stop sending invite to participant team if they user's email is in blocked email domains
- Added checks in `challenges/utils.py` for `blocked email domains` and `allowed email domains`.
- Updated `add_participant_to_team` with these common checks.
- Added checks in `invite_participant_to_team` api with these checks.
* Renamed the checks name
* Minor change
* Add check for banned email ids
* minor change
* Minor changes
* Added initial tests
* Updated test_views
* Updated test
* Updated tests
* Updated tests
* Updated tests and api
* Updated tests
---
apps/challenges/utils.py | 17 +++
apps/challenges/views.py | 31 ++---
apps/participants/views.py | 54 +++++++++
tests/unit/participants/test_views.py | 165 +++++++++++++++++++++++++-
4 files changed, 248 insertions(+), 19 deletions(-)
diff --git a/apps/challenges/utils.py b/apps/challenges/utils.py
index 3294deb0fc..9e6e777228 100644
--- a/apps/challenges/utils.py
+++ b/apps/challenges/utils.py
@@ -189,3 +189,20 @@ def create_federated_user(name, repository, aws_keys):
DurationSeconds=43200,
)
return response
+
+
+def is_user_in_allowed_email_domains(email, challenge_pk):
+ challenge = get_challenge_model(challenge_pk)
+ for domain in challenge.allowed_email_domains:
+ if domain.lower() in email.lower():
+ return True
+ return False
+
+
+def is_user_in_blocked_email_domains(email, challenge_pk):
+ challenge = get_challenge_model(challenge_pk)
+ for domain in challenge.blocked_email_domains:
+ domain = "@" + domain
+ if domain.lower() in email.lower():
+ return True
+ return False
diff --git a/apps/challenges/views.py b/apps/challenges/views.py
index 4c3b4bfea3..7bfac4665e 100644
--- a/apps/challenges/views.py
+++ b/apps/challenges/views.py
@@ -52,6 +52,8 @@
get_challenge_phase_split_model,
get_dataset_split_model,
get_leaderboard_model,
+ is_user_in_allowed_email_domains,
+ is_user_in_blocked_email_domains
)
from hosts.models import ChallengeHost, ChallengeHostTeam
from hosts.utils import (
@@ -273,12 +275,7 @@ def add_participant_team_to_challenge(
# Check if user is in allowed list.
user_email = request.user.email
if len(challenge.allowed_email_domains) > 0:
- present = False
- for domain in challenge.allowed_email_domains:
- if domain.lower() in user_email.lower():
- present = True
- break
- if not present:
+ if not is_user_in_allowed_email_domains(user_email, challenge_pk):
message = "Sorry, users with {} email domain(s) are only allowed to participate in this challenge."
domains = ""
for domain in challenge.allowed_email_domains:
@@ -290,18 +287,16 @@ def add_participant_team_to_challenge(
)
# Check if user is in blocked list.
- for domain in challenge.blocked_email_domains:
- domain = "@" + domain
- if domain.lower() in user_email.lower():
- message = "Sorry, users with {} email domain(s) are not allowed to participate in this challenge."
- domains = ""
- for domain in challenge.blocked_email_domains:
- domains = "{}{}{}".format(domains, "/", domain)
- domains = domains[1:]
- response_data = {"error": message.format(domains)}
- return Response(
- response_data, status=status.HTTP_406_NOT_ACCEPTABLE
- )
+ if is_user_in_blocked_email_domains(user_email, challenge_pk):
+ message = "Sorry, users with {} email domain(s) are not allowed to participate in this challenge."
+ domains = ""
+ for domain in challenge.blocked_email_domains:
+ domains = "{}{}{}".format(domains, "/", domain)
+ domains = domains[1:]
+ response_data = {"error": message.format(domains)}
+ return Response(
+ response_data, status=status.HTTP_406_NOT_ACCEPTABLE
+ )
# check to disallow the user if he is a Challenge Host for this challenge
participant_team_user_ids = set(
diff --git a/apps/participants/views.py b/apps/participants/views.py
index 9629490563..43a2e16a58 100644
--- a/apps/participants/views.py
+++ b/apps/participants/views.py
@@ -17,6 +17,11 @@
from base.utils import paginated_queryset
from challenges.models import Challenge
from challenges.serializers import ChallengeSerializer
+from challenges.utils import (
+ get_challenge_model,
+ is_user_in_allowed_email_domains,
+ is_user_in_blocked_email_domains
+)
from hosts.utils import is_user_a_host_of_challenge
from .models import Participant, ParticipantTeam
@@ -197,6 +202,55 @@ def invite_participant_to_team(request, pk):
}
return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE)
+ if len(team_participated_challenges) > 0:
+ for challenge_pk in team_participated_challenges:
+ challenge = get_challenge_model(challenge_pk)
+
+ if len(challenge.banned_email_ids) > 0:
+ # Check if team participants emails are banned
+ for participant_email in participant_team.get_all_participants_email():
+ if participant_email in challenge.banned_email_ids:
+ message = "You cannot invite as you're a part of {} team and it has been banned "
+ "from this challenge. Please contact the challenge host."
+ response_data = {"error": message.format(participant_team.team_name)}
+ return Response(
+ response_data, status=status.HTTP_406_NOT_ACCEPTABLE
+ )
+
+ # Check if invited user is banned
+ if email in challenge.banned_email_ids:
+ message = "You cannot invite as the invited user has been banned "
+ "from this challenge. Please contact the challenge host."
+ response_data = {"error": message}
+ return Response(
+ response_data, status=status.HTTP_406_NOT_ACCEPTABLE
+ )
+
+ # Check if user is in allowed list.
+ if len(challenge.allowed_email_domains) > 0:
+ if not is_user_in_allowed_email_domains(email, challenge_pk):
+ message = "Sorry, users with {} email domain(s) are only allowed to participate in this challenge."
+ domains = ""
+ for domain in challenge.allowed_email_domains:
+ domains = "{}{}{}".format(domains, "/", domain)
+ domains = domains[1:]
+ response_data = {"error": message.format(domains)}
+ return Response(
+ response_data, status=status.HTTP_406_NOT_ACCEPTABLE
+ )
+
+ # Check if user is in blocked list.
+ if is_user_in_blocked_email_domains(email, challenge_pk):
+ message = "Sorry, users with {} email domain(s) are not allowed to participate in this challenge."
+ domains = ""
+ for domain in challenge.blocked_email_domains:
+ domains = "{}{}{}".format(domains, "/", domain)
+ domains = domains[1:]
+ response_data = {"error": message.format(domains)}
+ return Response(
+ response_data, status=status.HTTP_406_NOT_ACCEPTABLE
+ )
+
serializer = InviteParticipantToTeamSerializer(
data=request.data,
context={"participant_team": participant_team, "request": request},
diff --git a/tests/unit/participants/test_views.py b/tests/unit/participants/test_views.py
index f7028f58d5..ce6aaec5cf 100644
--- a/tests/unit/participants/test_views.py
+++ b/tests/unit/participants/test_views.py
@@ -255,6 +255,34 @@ def test_particular_participant_team_delete(self):
class InviteParticipantToTeamTest(BaseAPITestClass):
def setUp(self):
super(InviteParticipantToTeamTest, self).setUp()
+
+ self.user1 = User.objects.create(
+ username="user1",
+ email="user1@platform.com",
+ password="user1_password",
+ )
+
+ EmailAddress.objects.create(
+ user=self.user1,
+ email="user1@platform.com",
+ primary=True,
+ verified=True,
+ )
+
+ self.participant_team1 = ParticipantTeam.objects.create(
+ team_name="Team A", created_by=self.user1
+ )
+
+ self.participant1 = Participant.objects.create(
+ user=self.user1,
+ status=Participant.ACCEPTED,
+ team=self.participant_team1,
+ )
+
+ self.challenge_host_team = ChallengeHostTeam.objects.create(
+ team_name="Host Team 1", created_by=self.user1
+ )
+
self.data = {"email": self.invite_user.email}
self.url = reverse_lazy(
"participants:invite_participant_to_team",
@@ -308,13 +336,148 @@ def test_invite_user_which_does_not_exist_to_team(self):
def test_particular_participant_team_for_invite_does_not_exist(self):
self.url = reverse_lazy(
"participants:invite_participant_to_team",
- kwargs={"pk": self.participant_team.pk + 1},
+ kwargs={"pk": self.participant_team.pk + 2},
)
expected = {"error": "Participant Team does not exist"}
response = self.client.post(self.url, {})
self.assertEqual(response.data, expected)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+ def test_invite_when_team_participants_emails_are_banned(self):
+ self.challenge1 = Challenge.objects.create(
+ title="Test Challenge 1",
+ short_description="Short description for test challenge 1",
+ description="Description for test challenge 1",
+ terms_and_conditions="Terms and conditions for test challenge 1",
+ submission_guidelines="Submission guidelines for test challenge 1",
+ creator=self.challenge_host_team,
+ published=False,
+ is_registration_open=True,
+ enable_forum=True,
+ banned_email_ids=["user1@platform.com"],
+ leaderboard_description="Lorem ipsum dolor sit amet, consectetur adipiscing elit",
+ anonymous_leaderboard=False,
+ start_date=timezone.now() - timedelta(days=2),
+ end_date=timezone.now() + timedelta(days=1),
+ )
+
+ self.challenge1.participant_teams.add(self.participant_team1)
+ self.data = {"email": self.invite_user.email}
+ self.client.force_authenticate(user=self.user1)
+ self.url = reverse_lazy(
+ "participants:invite_participant_to_team",
+ kwargs={
+ "pk": self.participant_team1.pk
+ },
+ )
+
+ response = self.client.post(self.url, self.data)
+ message = "You cannot invite as you're a part of {} team and it has been banned "
+ "from this challenge. Please contact the challenge host."
+ expected = {"error": message.format(self.participant_team1.team_name)}
+ self.assertEqual(response.data, expected)
+ self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE)
+
+ def test_invite_when_invited_user_is_banned(self):
+ self.challenge1 = Challenge.objects.create(
+ title="Test Challenge 1",
+ short_description="Short description for test challenge 1",
+ description="Description for test challenge 1",
+ terms_and_conditions="Terms and conditions for test challenge 1",
+ submission_guidelines="Submission guidelines for test challenge 1",
+ creator=self.challenge_host_team,
+ published=False,
+ is_registration_open=True,
+ enable_forum=True,
+ banned_email_ids=["other@platform.com"],
+ leaderboard_description="Lorem ipsum dolor sit amet, consectetur adipiscing elit",
+ anonymous_leaderboard=False,
+ start_date=timezone.now() - timedelta(days=2),
+ end_date=timezone.now() + timedelta(days=1),
+ )
+ self.challenge1.participant_teams.add(self.participant_team1)
+ self.data = {"email": self.invite_user.email}
+ self.client.force_authenticate(user=self.user1)
+ self.url = reverse_lazy(
+ "participants:invite_participant_to_team",
+ kwargs={
+ "pk": self.participant_team1.pk
+ },
+ )
+ response = self.client.post(self.url, self.data)
+ message = "You cannot invite as the invited user has been banned "
+ "from this challenge. Please contact the challenge host."
+ expected = {"error": message}
+ self.assertEqual(response.data, expected)
+ self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE)
+
+ def test_invite_when_invited_user_is_in_blocked_domains(self):
+ self.challenge1 = Challenge.objects.create(
+ title="Test Challenge 1",
+ short_description="Short description for test challenge 1",
+ description="Description for test challenge 1",
+ terms_and_conditions="Terms and conditions for test challenge 1",
+ submission_guidelines="Submission guidelines for test challenge 1",
+ creator=self.challenge_host_team,
+ published=False,
+ is_registration_open=True,
+ enable_forum=True,
+ blocked_email_domains=["platform"],
+ leaderboard_description="Lorem ipsum dolor sit amet, consectetur adipiscing elit",
+ anonymous_leaderboard=False,
+ start_date=timezone.now() - timedelta(days=2),
+ end_date=timezone.now() + timedelta(days=1),
+ )
+ self.challenge1.participant_teams.add(self.participant_team1)
+ self.data = {"email": self.invite_user.email}
+ self.client.force_authenticate(user=self.user1)
+ self.url = reverse_lazy(
+ "participants:invite_participant_to_team",
+ kwargs={
+ "pk": self.participant_team1.pk
+ },
+ )
+
+ response = self.client.post(self.url, self.data)
+ message = "Sorry, users with {} email domain(s) are not allowed to participate in this challenge."
+ expected = {"error": message.format("platform")}
+ self.assertEqual(response.data, expected)
+ self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE)
+
+ def test_invite_when_invited_user_is_not_in_allowed_domains(self):
+ self.challenge1 = Challenge.objects.create(
+ title="Test Challenge 1",
+ short_description="Short description for test challenge 1",
+ description="Description for test challenge 1",
+ terms_and_conditions="Terms and conditions for test challenge 1",
+ submission_guidelines="Submission guidelines for test challenge 1",
+ creator=self.challenge_host_team,
+ published=False,
+ is_registration_open=True,
+ enable_forum=True,
+ allowed_email_domains=["example1"],
+ leaderboard_description="Lorem ipsum dolor sit amet, consectetur adipiscing elit",
+ anonymous_leaderboard=False,
+ start_date=timezone.now() - timedelta(days=2),
+ end_date=timezone.now() + timedelta(days=1),
+ )
+ self.challenge1.participant_teams.add(self.participant_team1)
+ self.data = {"email": self.invite_user.email}
+ self.client.force_authenticate(user=self.user1)
+ self.url = reverse_lazy(
+ "participants:invite_participant_to_team",
+ kwargs={
+ "pk": self.participant_team1.pk
+ },
+ )
+
+ response = self.client.post(self.url, self.data)
+ message = "Sorry, users with {} email domain(s) are only allowed to participate in this challenge."
+ expected = {"error": message.format("example1")}
+
+ self.assertEqual(response.data, expected)
+ self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE)
+
def test_invite_participant_to_team_when_user_cannot_be_invited(self):
"""
NOTE
From cc11f7ac8d5180cf8ecb8604e205954cea70721e Mon Sep 17 00:00:00 2001
From: Andre Christoga Pramaditya
Date: Mon, 16 Dec 2019 05:03:28 +0700
Subject: [PATCH 03/34] Docs: Fix GitHub name in the docs(#2514)
---
docs/source/faq(developers).md | 2 +-
frontend/src/views/web/get-involved.html | 2 +-
frontend/src/views/web/profile.html | 2 +-
frontend/src/views/web/update-profile.html | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/source/faq(developers).md b/docs/source/faq(developers).md
index 0c3ef62934..e3d006a4ce 100644
--- a/docs/source/faq(developers).md
+++ b/docs/source/faq(developers).md
@@ -9,7 +9,7 @@ Alternatively, if you come across a new bug on the site, please file a new issue
Please refer to [Technologies Used](https://evalai.readthedocs.io/en/latest/architecture.html)
-#### Q. Where could I learn Github Commands?
+#### Q. Where could I learn GitHub Commands?
Refer to [GitHub Guide](https://help.github.com/articles/git-and-github-learning-resources/).
diff --git a/frontend/src/views/web/get-involved.html b/frontend/src/views/web/get-involved.html
index 2748dabe8f..c3d4c288c7 100644
--- a/frontend/src/views/web/get-involved.html
+++ b/frontend/src/views/web/get-involved.html
@@ -16,7 +16,7 @@ Report issues
EvalAI Google Group ,
or contact us at team@cloudcv.org .
Improving and maintaining the site
- The EvalAI project is fully open source, and is maintained by a large community of volunteers on Github .
+
The EvalAI project is fully open source, and is maintained by a large community of volunteers on GitHub .
We are in need of coders and designers so if you would like to help out, please drop us a line!
The best way to get started is to write us at team@cloudcv.org
or ping us on our Gitter Channel .
diff --git a/frontend/src/views/web/profile.html b/frontend/src/views/web/profile.html
index e810b8fb9f..9859d223cd 100644
--- a/frontend/src/views/web/profile.html
+++ b/frontend/src/views/web/profile.html
@@ -43,7 +43,7 @@
- Github Url
+ GitHub Url
- Github Url
+ GitHub Url
{{updateProfile.errorResponse.data.github_url[0]}}
From 66f8fae0f7fe48abc6342700af8dc6feeda76faa Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Sun, 15 Dec 2019 17:41:04 -0500
Subject: [PATCH 04/34] Backend: Add search field in participant team
admin(#2517)
---
apps/participants/admin.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/participants/admin.py b/apps/participants/admin.py
index afc7efb606..8cf646596f 100644
--- a/apps/participants/admin.py
+++ b/apps/participants/admin.py
@@ -22,7 +22,7 @@ class ParticipantAdmin(ImportExportTimeStampedAdmin):
list_display = ("user", "status", "team")
search_fields = ("user__username", "status", "team__team_name")
- list_filter = ("status", "team")
+ list_filter = ("status",)
resource_class = ParticipantResource
@@ -34,4 +34,4 @@ class ParticipantTeamAdmin(ImportExportTimeStampedAdmin):
"""
list_display = ("team_name", "get_all_participants_email", "team_url")
- list_filter = ("team_name",)
+ search_fields = ("team_name", "team_url", "created_by__username")
From 70dd6848a2fa26564d05f64459e6448ed057db45 Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Thu, 19 Dec 2019 19:33:36 -0500
Subject: [PATCH 05/34] Update requirements: Add django-allauth-0.40.0 in
common.txt to support django-1.11.23(#2525)
---
requirements/common.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements/common.txt b/requirements/common.txt
index 6d6c6ac2fa..7db8dbedcf 100644
--- a/requirements/common.txt
+++ b/requirements/common.txt
@@ -4,6 +4,7 @@ botocore==1.12.88
celery[sqs]==4.3.0
commonmark==0.5.4
django==1.11.23
+django-allauth==0.40.0
django-filter==2.1.0
django-import-export==0.5.1
djangorestframework==3.9.3
From 27a1dd11040d884af0a8843be3859d09f0c72092 Mon Sep 17 00:00:00 2001
From: Kartik Verma
Date: Sun, 22 Dec 2019 10:13:21 +0530
Subject: [PATCH 06/34] Backend: Mock AWS ECR requests for dev and test
environment(#2359)
* Development Environment for Docker based challenges
Signed-off-by: Kartik Verma
* Defualt value for AWS_SECRET_ACCESS_KEY
Signed-off-by: Kartik Verma
* Added moto to requirements
Signed-off-by: Kartik Verma
* Updating botocore and adding moto's working commit to requirements
Signed-off-by: Kartik Verma
* merge branch mock-aws-credentials-dev-unittest (#73)
* Added moto's release
Signed-off-by: vkartik97 <3920286+vkartik97@users.noreply.github.com>
* Remove moto from dev.txt
Signed-off-by: vkartik97 <3920286+vkartik97@users.noreply.github.com>
* Update PyYaml
Signed-off-by: vkartik97 <3920286+vkartik97@users.noreply.github.com>
* Reverting to stable versions
Signed-off-by: vkartik97 <3920286+vkartik97@users.noreply.github.com>
* [Kartik] Refactored AWS Mocker
* Allow to bypass Mocker if AWS Credentials are set
* Docs for methods
Signed-off-by: vkartik97 <3920286+vkartik97@users.noreply.github.com>
* Revert bypass commit
Signed-off-by: vkartik97 <3920286+vkartik97@users.noreply.github.com>
* Add review comments
* Fix flake8 changes
Co-authored-by: Rishabh Jain
---
apps/base/utils.py | 8 +++
apps/challenges/utils.py | 54 +++++++++++++++--
apps/challenges/views.py | 39 +++++-------
requirements/common.txt | 1 +
requirements/dev.txt | 1 -
tests/unit/challenges/test_urls.py | 9 +++
tests/unit/challenges/test_views.py | 93 +++++++++++++++++++++++++++++
7 files changed, 174 insertions(+), 31 deletions(-)
diff --git a/apps/base/utils.py b/apps/base/utils.py
index 4a5d6aefd5..f54c1de14f 100644
--- a/apps/base/utils.py
+++ b/apps/base/utils.py
@@ -228,3 +228,11 @@ def send_slack_notification(webhook=settings.SLACK_WEB_HOOK_URL, message=""):
logger.exception(
"Exception raised while sending slack notification. \n Exception message: {}".format(e)
)
+
+
+def mock_if_non_prod_aws(aws_mocker):
+ def decorator(func):
+ if not (settings.DEBUG or settings.TEST):
+ return func
+ return aws_mocker(func)
+ return decorator
diff --git a/apps/challenges/utils.py b/apps/challenges/utils.py
index 9e6e777228..dfdab5491c 100644
--- a/apps/challenges/utils.py
+++ b/apps/challenges/utils.py
@@ -1,11 +1,16 @@
import os
-
import json
import logging
+import uuid
from botocore.exceptions import ClientError
+from moto import mock_ecr, mock_sts
-from base.utils import get_model_object, get_boto3_client
+from base.utils import (
+ get_model_object,
+ get_boto3_client,
+ mock_if_non_prod_aws,
+)
from .models import (
Challenge,
@@ -81,9 +86,9 @@ def get_aws_credentials_for_challenge(challenge_pk):
}
else:
aws_keys = {
- "AWS_ACCOUNT_ID": os.environ.get("AWS_ACCOUNT_ID"),
- "AWS_ACCESS_KEY_ID": os.environ.get("AWS_ACCESS_KEY_ID"),
- "AWS_SECRET_ACCESS_KEY": os.environ.get("AWS_SECRET_ACCESS_KEY"),
+ "AWS_ACCOUNT_ID": os.environ.get("AWS_ACCOUNT_ID", "aws_account_id"),
+ "AWS_ACCESS_KEY_ID": os.environ.get("AWS_ACCESS_KEY_ID", "aws_access_key_id"),
+ "AWS_SECRET_ACCESS_KEY": os.environ.get("AWS_SECRET_ACCESS_KEY", "aws_secret_access_key"),
"AWS_REGION": os.environ.get("AWS_DEFAULT_REGION", "us-east-1"),
}
return aws_keys
@@ -119,7 +124,8 @@ def get_or_create_ecr_repository(name, aws_keys):
)
repository = response["repositories"][0]
except ClientError as e:
- if e.response["Error"]["Code"] == "RepositoryNotFoundException":
+ if e.response["Error"]["Code"] == "RepositoryNotFoundException" or\
+ e.response["Error"]["Code"] == "400":
response = client.create_repository(repositoryName=name)
repository = response["repository"]
created = True
@@ -191,6 +197,42 @@ def create_federated_user(name, repository, aws_keys):
return response
+@mock_if_non_prod_aws(mock_ecr)
+@mock_if_non_prod_aws(mock_sts)
+def get_aws_credentials_for_submission(challenge, participant_team):
+ """
+ Method to generate AWS Credentails for CLI's Push
+ Wrappers:
+ - mock_ecr: To mock ECR requests to generate ecr credemntials
+ - mock_sts: To mock STS requests to generated federated user
+ Args:
+ - challenge: Challenge model
+ - participant_team: Participant Team Model
+ Returns:
+ - dict: {
+ "federated_user"
+ "docker_repository_uri"
+ }
+ """
+ aws_keys = get_aws_credentials_for_challenge(challenge.pk)
+ ecr_repository_name = "{}-participant-team-{}".format(
+ challenge.slug, participant_team.pk
+ )
+ ecr_repository_name = convert_to_aws_ecr_compatible_format(
+ ecr_repository_name
+ )
+ repository, created = get_or_create_ecr_repository(
+ ecr_repository_name, aws_keys
+ )
+ name = str(uuid.uuid4())[:32]
+ docker_repository_uri = repository["repositoryUri"]
+ federated_user = create_federated_user(name, ecr_repository_name, aws_keys)
+ return {
+ "federated_user": federated_user,
+ "docker_repository_uri": docker_repository_uri,
+ }
+
+
def is_user_in_allowed_email_domains(email, challenge_pk):
challenge = get_challenge_model(challenge_pk)
for domain in challenge.allowed_email_domains:
diff --git a/apps/challenges/views.py b/apps/challenges/views.py
index 7bfac4665e..2fb695c462 100644
--- a/apps/challenges/views.py
+++ b/apps/challenges/views.py
@@ -98,11 +98,8 @@
ZipChallengePhaseSplitSerializer,
)
from .utils import (
- create_federated_user,
- convert_to_aws_ecr_compatible_format,
- get_aws_credentials_for_challenge,
get_file_content,
- get_or_create_ecr_repository,
+ get_aws_credentials_for_submission,
)
logger = logging.getLogger(__name__)
@@ -1896,8 +1893,19 @@ def get_broker_url_by_challenge_pk(request, challenge_pk):
@authentication_classes((ExpiringTokenAuthentication,))
def get_aws_credentials_for_participant_team(request, phase_pk):
"""
- Returns:
- Dictionary containing AWS credentials for the participant team for a particular challenge
+ Endpoint to generate AWS Credentails for CLI
+ Args:
+ - challenge: Challenge model
+ - participant_team: Participant Team Model
+ Returns:
+ - JSON: {
+ "federated_user"
+ "docker_repository_uri"
+ }
+ Raises:
+ - BadRequestException 400
+ - When participant_team has not participanted in challenge
+ - When Challenge is not Docker based
"""
challenge_phase = get_challenge_phase_model(phase_pk)
challenge = challenge_phase.challenge
@@ -1915,24 +1923,7 @@ def get_aws_credentials_for_participant_team(request, phase_pk):
"error": "You have not participated in this challenge."
}
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
-
- aws_keys = get_aws_credentials_for_challenge(challenge.pk)
- ecr_repository_name = "{}-participant-team-{}".format(
- challenge.slug, participant_team.pk
- )
- ecr_repository_name = convert_to_aws_ecr_compatible_format(
- ecr_repository_name
- )
- repository, created = get_or_create_ecr_repository(
- ecr_repository_name, aws_keys
- )
- name = str(uuid.uuid4())[:32]
- docker_repository_uri = repository["repositoryUri"]
- federated_user = create_federated_user(name, ecr_repository_name, aws_keys)
- data = {
- "federated_user": federated_user,
- "docker_repository_uri": docker_repository_uri,
- }
+ data = get_aws_credentials_for_submission(challenge, participant_team)
response_data = {"success": data}
return Response(response_data, status=status.HTTP_200_OK)
diff --git a/requirements/common.txt b/requirements/common.txt
index 7db8dbedcf..78e039587c 100644
--- a/requirements/common.txt
+++ b/requirements/common.txt
@@ -15,6 +15,7 @@ django-ses==0.8.5
docker-compose==1.21.0
drfdocs==0.0.11
drf-yasg==1.11.0
+moto==1.3.8
pika==0.10.0
pickleshare==0.7.4
Pillow==6.2.0
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 94dce62c83..c97d86bafd 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -10,7 +10,6 @@ django-silk==1.0.0
django-spaghetti-and-meatballs==0.2.2
flake8==3.0.4
mock==2.0.0
-moto==1.3.8
pre-commit==1.14.4
pytest==3.5.0
pytest-cov==2.5.1
diff --git a/tests/unit/challenges/test_urls.py b/tests/unit/challenges/test_urls.py
index c0ca6ad79d..9fcada33db 100644
--- a/tests/unit/challenges/test_urls.py
+++ b/tests/unit/challenges/test_urls.py
@@ -282,6 +282,15 @@ def test_challenges_urls(self):
resolver = resolve(self.url)
self.assertEqual(resolver.view_name, "challenges:star_challenge")
+ self.url = reverse_lazy(
+ "challenges:get_aws_credentials_for_participant_team",
+ kwargs={"phase_pk": self.challenge_phase.pk},
+ )
+ self.assertEqual(
+ self.url, "/api/challenges/phases/{}/participant_team/aws/credentials/".format(self.challenge_phase.pk)
+ )
+ resolver = resolve(self.url)
+ self.assertEqual(resolver.view_name, "challenges:get_aws_credentials_for_participant_team")
self.url = reverse_lazy(
'challenges:get_challenge_phase_by_pk',
kwargs={'pk': self.challenge_phase.pk},
diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py
index 6762fdc05e..f87819718a 100644
--- a/tests/unit/challenges/test_views.py
+++ b/tests/unit/challenges/test_views.py
@@ -4012,3 +4012,96 @@ def test_get_challenge_phases_by_challenge_pk_when_user_is_not_challenge_host(
response = self.client.get(self.url, {})
self.assertEqual(response.data, expected)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+
+class GetAWSCredentialsForParticipantTeamTest(BaseChallengePhaseClass):
+ def setUp(self):
+ super(GetAWSCredentialsForParticipantTeamTest, self).setUp()
+ self.url = reverse_lazy(
+ "challenges:star_challenge",
+ kwargs={"challenge_pk": self.challenge.pk},
+ )
+
+ self.user1 = User.objects.create(
+ username="otheruser1",
+ password="other_secret_password",
+ email="user1@test.com",
+ )
+
+ self.user2 = User.objects.create(
+ username="otheruser2",
+ password="other_secret_password",
+ email="user2@test.com",
+ )
+
+ EmailAddress.objects.create(
+ user=self.user1,
+ email="user1@test.com",
+ primary=True,
+ verified=True,
+ )
+
+ EmailAddress.objects.create(
+ user=self.user2,
+ email="user2@test.com",
+ primary=True,
+ verified=True,
+ )
+
+ self.participant_team = ParticipantTeam.objects.create(
+ team_name="Participant Team for Challenge8", created_by=self.user1
+ )
+
+ self.participant1 = Participant.objects.create(
+ user=self.user1,
+ status=Participant.ACCEPTED,
+ team=self.participant_team,
+ )
+ self.challenge.participant_teams.add(self.participant_team)
+ self.client.force_authenticate(user=self.user1)
+ self.challenge.is_docker_based = True
+ self.challenge.save()
+
+ def test_get_aws_credentials_when_challenge_is_not_docker_based(self):
+ self.challenge.is_docker_based = False
+ self.challenge.save()
+
+ self.url = reverse_lazy(
+ "challenges:get_aws_credentials_for_participant_team",
+ kwargs={"phase_pk": self.challenge_phase.pk},
+ )
+ expected = {
+ "error": "Sorry, this is not a docker based challenge."
+ }
+ response = self.client.get(self.url, {})
+ self.assertEqual(response.data, expected)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_get_aws_credentials_when_not_participated(self):
+ self.url = reverse_lazy(
+ "challenges:get_aws_credentials_for_participant_team",
+ kwargs={"phase_pk": self.challenge_phase.pk},
+ )
+ expected = {
+ "error": "You have not participated in this challenge."
+ }
+ self.client.force_authenticate(user=self.user2)
+ response = self.client.get(self.url, {})
+ self.assertEqual(response.data, expected)
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_get_aws_credentials(self):
+ self.url = reverse_lazy(
+ "challenges:get_aws_credentials_for_participant_team",
+ kwargs={"phase_pk": self.challenge_phase.pk},
+ )
+ response = self.client.get(self.url, {})
+ data = response.data
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # Check if all fields for cli exists
+ self.assertTrue("federated_user" in data["success"])
+ self.assertTrue("docker_repository_uri" in data["success"])
+ federated_user = data["success"]["federated_user"]
+ self.assertTrue("AccessKeyId" in federated_user["Credentials"])
+ self.assertTrue("SecretAccessKey" in federated_user["Credentials"])
+ self.assertTrue("SessionToken" in federated_user["Credentials"])
From 834da106731ff2c7b84a6eebf36fb927a5d90703 Mon Sep 17 00:00:00 2001
From: Kartik Verma
Date: Mon, 23 Dec 2019 00:21:28 +0530
Subject: [PATCH 07/34] Workers: Add bash script to make EKS Kube Config for RL
Worker(#2410)
* Added script to make kube config for RL Worker
* Moved rl_kube_warmup to scripts/worker
Co-authored-by: Rishabh Jain
---
scripts/workers/rl_kube_warmup.sh | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 scripts/workers/rl_kube_warmup.sh
diff --git a/scripts/workers/rl_kube_warmup.sh b/scripts/workers/rl_kube_warmup.sh
new file mode 100644
index 0000000000..f2c698c5df
--- /dev/null
+++ b/scripts/workers/rl_kube_warmup.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+# Usage:
+# bash rl_worker_kube_warmup.sh [FLAGS]
+#
+# Environment Variables to be passed:
+# CLUSTER_NAME: Name of EKS Cluster
+#
+# Flags:
+# new-config: To overwrite kube config. Can also be used if kube config is not present
+
+RED='\033[0;31m'
+NC='\033[0m'
+KUBE_CONFIG_PATH=$HOME/.kube/config
+
+if [[ ! -f "$KUBE_CONFIG_PATH" ]] || [[ $* == --new-config ]]; then
+ if [[ -z "${CLUSTER_NAME}" ]]; then
+ echo -e "${RED}ERROR: CLUSTER_NAME variable not set!${NC}"
+ exit 1
+ fi
+ aws eks update-kubeconfig --name $CLUSTER_NAME
+fi
From 045728e3e68f12e3bd65ef068c09cc3c11f2915d Mon Sep 17 00:00:00 2001
From: Takitsuse Nagisa <57856193+takitsuse@users.noreply.github.com>
Date: Mon, 23 Dec 2019 04:10:05 +0900
Subject: [PATCH 08/34] Remote Worker: Add test for
create_dir_as_python_package function(#2509)
* Add test for create_dir_as_python_package
* Add assertion that checks if file is deleted successfully
Co-authored-by: Rishabh Jain
---
tests/unit/remoteworker/test_remote_worker.py | 24 +++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/tests/unit/remoteworker/test_remote_worker.py b/tests/unit/remoteworker/test_remote_worker.py
index ca355972d1..c3e1de0dd2 100644
--- a/tests/unit/remoteworker/test_remote_worker.py
+++ b/tests/unit/remoteworker/test_remote_worker.py
@@ -1,8 +1,14 @@
import mock
+import os
+import shutil
+import tempfile
+
+from os.path import join
from unittest import TestCase
from scripts.workers.remote_submission_worker import (
+ create_dir_as_python_package,
make_request,
get_message_from_sqs_queue,
delete_message_from_sqs_queue,
@@ -141,3 +147,21 @@ def test_return_url_per_environment(self):
expected_url = "http://testserver:80{}".format(url)
returned_url = return_url_per_environment(url)
self.assertEqual(returned_url, expected_url)
+
+
+class CreateDirAsPythonPackageTest(BaseTestClass):
+ def setUp(self):
+ super(CreateDirAsPythonPackageTest, self).setUp()
+
+ self.BASE_TEMP_DIR = tempfile.mkdtemp()
+ self.temp_directory = join(self.BASE_TEMP_DIR, "temp_dir")
+
+ def test_create_dir_as_python_package(self):
+ create_dir_as_python_package(self.temp_directory)
+ self.assertTrue(os.path.isfile(join(self.temp_directory, "__init__.py")))
+
+ with open(join(self.temp_directory, "__init__.py")) as f:
+ self.assertEqual(f.read(), "")
+
+ shutil.rmtree(self.temp_directory)
+ self.assertFalse(os.path.exists(self.temp_directory))
From 8cfb91a91793a6f1ac051762fed62ee7d3e3b8a6 Mon Sep 17 00:00:00 2001
From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com>
Date: Mon, 23 Dec 2019 00:52:57 +0530
Subject: [PATCH 09/34] Worker: Add tests for extract_submission_data
function(#2498)
* [GCI] Add test for extract_submission_data
Added `test_extract_submission_data` to test function [`extract_submission_data` ](https://github.com/Cloud-CV/EvalAI/blob/master/scripts/workers/submission_worker.py#L275) from submission worker.
* Update test_submission_worker.py
* Fix typo
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Make requested changes
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Added access to db
* Added access to django db by changing `unittest.TestCase` to `rest_framework.APITestCase`.
* Made code compatible with https://github.com/Cloud-CV/EvalAI/pulls/2497
* Fix typo
ibjects -> objects
* Make requested changes, add new class variables
* Remove start and stop calls to patcher.
* Revert "Remove start and stop calls to patcher."
This reverts commit fd7d63b0a6de13df14a3b7ba02e5f1706c291a8d.
* Fix failing Travis CI build
* Update test_submission_worker.py
* Update test_submission_worker.py
* Update test_submission_worker.py
* Fix flake8 errors and failing Travis build
* Update test_submission_worker.py
* Make requested changes, remove unused variables and method.
Co-authored-by: Rishabh Jain
---
tests/unit/worker/test_submission_worker.py | 115 +++++++++++++++++++-
1 file changed, 113 insertions(+), 2 deletions(-)
diff --git a/tests/unit/worker/test_submission_worker.py b/tests/unit/worker/test_submission_worker.py
index 7b5597b033..166f3a1e7f 100644
--- a/tests/unit/worker/test_submission_worker.py
+++ b/tests/unit/worker/test_submission_worker.py
@@ -14,11 +14,17 @@
from rest_framework.test import APITestCase
-from challenges.models import Challenge
+from challenges.models import (
+ Challenge,
+ ChallengePhase,
+)
from hosts.models import ChallengeHostTeam
+from jobs.models import Submission
+from participants.models import ParticipantTeam
from scripts.workers.submission_worker import (
create_dir,
create_dir_as_python_package,
+ extract_submission_data,
load_challenge_and_return_max_submissions,
return_file_url_per_environment,
get_or_create_sqs_queue
@@ -27,12 +33,22 @@
class BaseAPITestClass(APITestCase):
def setUp(self):
+
self.BASE_TEMP_DIR = tempfile.mkdtemp()
+
+ self.SUBMISSION_DATA_DIR = join(
+ self.BASE_TEMP_DIR, "compute/submission_files/submission_{submission_id}"
+ )
+
self.temp_directory = join(self.BASE_TEMP_DIR, "temp_dir")
+
+ self.testserver = "http://testserver"
self.url = "/test/url"
+
self.input_file = open(join(self.BASE_TEMP_DIR, 'dummy_input.txt'), "w+")
self.input_file.write("file_content")
self.input_file.close()
+
self.sqs_client = boto3.client(
"sqs",
endpoint_url=os.environ.get("AWS_SQS_ENDPOINT", "http://sqs:9324"),
@@ -67,6 +83,57 @@ def setUp(self):
),
)
+ self.participant_team = ParticipantTeam.objects.create(
+ team_name="Some Participant Team", created_by=self.user
+ )
+
+ self.challenge_phase = ChallengePhase.objects.create(
+ name="Challenge Phase",
+ description="Description for Challenge Phase",
+ leaderboard_public=False,
+ is_public=True,
+ start_date=timezone.now() - timedelta(days=2),
+ end_date=timezone.now() + timedelta(days=1),
+ challenge=self.challenge,
+ test_annotation=SimpleUploadedFile(
+ "test_sample_file.txt",
+ b"Dummy file content",
+ content_type="text/plain",
+ ),
+ max_submissions_per_day=100,
+ max_submissions_per_month=500,
+ max_submissions=1000,
+ codename="Phase Code Name",
+ )
+
+ self.submission = Submission.objects.create(
+ participant_team=self.participant_team,
+ challenge_phase=self.challenge_phase,
+ created_by=self.challenge_host_team.created_by,
+ status="submitted",
+ input_file=SimpleUploadedFile(
+ "test_sample_file.txt",
+ b"Dummy file content",
+ content_type="text/plain",
+ ),
+ method_name="Test Method",
+ method_description="Test Description",
+ project_url=self.testserver,
+ publication_url=self.testserver,
+ is_public=True,
+ is_flagged=True,
+ )
+
+ def get_submission_input_file_path(self, submission_id, input_file):
+ """Helper Method: Takes `submission_id` and `input_file` as input and
+ returns corresponding path to submmitted input file"""
+
+ input_file_name = os.path.basename(input_file.name)
+ return join(self.SUBMISSION_DATA_DIR, "{input_file}").format(
+ submission_id=submission_id,
+ input_file=input_file_name,
+ )
+
def test_create_dir(self):
create_dir(self.temp_directory)
self.assertTrue(os.path.isdir(self.temp_directory))
@@ -79,7 +146,51 @@ def test_create_dir_as_python_package(self):
def test_return_file_url_per_environment(self):
returned_url = return_file_url_per_environment(self.url)
- self.assertEqual(returned_url, "http://testserver/test/url")
+ self.assertEqual(returned_url, "{0}{1}".format(self.testserver, self.url))
+
+ @mock.patch("scripts.workers.submission_worker.create_dir_as_python_package")
+ @mock.patch("scripts.workers.submission_worker.download_and_extract_file")
+ def test_extract_submission_data_success(
+ self,
+ mock_download_and_extract_file,
+ mock_create_dir_as_python_package):
+ submission_input_file_path = join(
+ self.SUBMISSION_DATA_DIR, "{input_file}"
+ )
+ mock_submission_data_dir = mock.patch(
+ "scripts.workers.submission_worker.SUBMISSION_DATA_DIR",
+ self.SUBMISSION_DATA_DIR
+ )
+ mock_submission_input_file_path = mock.patch(
+ "scripts.workers.submission_worker.SUBMISSION_INPUT_FILE_PATH",
+ submission_input_file_path,
+ )
+ mock_submission_data_dir.start()
+ mock_submission_input_file_path.start()
+
+ submission = extract_submission_data(self.submission.pk)
+
+ expected_submission_data_dir = self.SUBMISSION_DATA_DIR.format(submission_id=self.submission.pk)
+ mock_create_dir_as_python_package.assert_called_with(expected_submission_data_dir)
+
+ expected_submission_input_file = "{0}{1}".format(self.testserver, self.submission.input_file.url)
+ expected_submission_input_file_path = self.get_submission_input_file_path(
+ self.submission.pk,
+ self.submission.input_file,
+ )
+ mock_download_and_extract_file.assert_called_with(expected_submission_input_file, expected_submission_input_file_path)
+
+ self.assertEqual(submission, self.submission)
+
+ mock_submission_data_dir.stop()
+ mock_submission_input_file_path.stop()
+
+ @mock.patch("scripts.workers.submission_worker.logger.critical")
+ def test_extract_submission_data_when_submission_does_not_exist(self, mock_logger):
+ non_existing_submission_pk = self.submission.pk + 1
+ value = extract_submission_data(non_existing_submission_pk)
+ mock_logger.assert_called_with("Submission {} does not exist".format(non_existing_submission_pk))
+ self.assertEqual(value, None)
@mock.patch("scripts.workers.submission_worker.load_challenge")
def test_load_challenge_and_return_max_submissions(self, mocked_load_challenge):
From 06662fc2c641bf233da733da446305dd8d1fca49 Mon Sep 17 00:00:00 2001
From: Kartik Verma
Date: Mon, 23 Dec 2019 01:23:20 +0530
Subject: [PATCH 10/34] Add RL Submission Worker and required util to interact
with API(#2415)
* [Kartik] Added RL Worker and required util to interact with API
Signed-off-by: vkartik97 <3920286+vkartik97@users.noreply.github.com>
* Replaced Print with logger
* Delay using Environment Variable
Co-authored-by: Rishabh Jain
---
scripts/workers/rl_submission_worker.py | 139 ++++++++++++++++++++++++
scripts/workers/worker_util.py | 104 ++++++++++++++++++
2 files changed, 243 insertions(+)
create mode 100644 scripts/workers/rl_submission_worker.py
create mode 100644 scripts/workers/worker_util.py
diff --git a/scripts/workers/rl_submission_worker.py b/scripts/workers/rl_submission_worker.py
new file mode 100644
index 0000000000..70203c899c
--- /dev/null
+++ b/scripts/workers/rl_submission_worker.py
@@ -0,0 +1,139 @@
+import logging
+import os
+import signal
+import time
+
+from .worker_util import (
+ EvalAI_Interface
+)
+
+from kubernetes import client, config
+
+
+class GracefulKiller:
+ kill_now = False
+
+ def __init__(self):
+ signal.signal(signal.SIGINT, self.exit_gracefully)
+ signal.signal(signal.SIGTERM, self.exit_gracefully)
+
+ def exit_gracefully(self, signum, frame):
+ self.kill_now = True
+
+
+logger = logging.getLogger(__name__)
+
+AUTH_TOKEN = os.environ.get("AUTH_TOKEN", "x")
+DJANGO_SERVER = os.environ.get("DJANGO_SERVER", "http://localhost")
+DJANGO_SERVER_PORT = os.environ.get("DJANGO_SERVER_PORT", "8000")
+QUEUE_NAME = os.environ.get("QUEUE_NAME", "evalai_submission_queue")
+ENVIRONMENT_IMAGE = os.environ.get("ENVIRONMENT_IMAGE", "x:tag")
+MESSAGE_FETCH_DEPLAY = int(os.environ.get("MESSAGE_FETCH_DEPLAY", "5"))
+
+
+def create_deployment_object(image, submission, message):
+ PYTHONUNBUFFERED_ENV = client.V1EnvVar(
+ name="PYTHONUNBUFFERED",
+ value="1",
+ )
+ AUTH_TOKEN_ENV = client.V1EnvVar(
+ name="AUTH_TOKEN",
+ value=AUTH_TOKEN
+ )
+ DJANGO_SERVER_ENV = client.V1EnvVar(
+ name="DJANGO_SERVER",
+ value=DJANGO_SERVER
+ )
+ MESSAGE_BODY_ENV = client.V1EnvVar(
+ name="BODY",
+ value=str(message)
+ )
+ agent_container = client.V1Container(
+ name="agent",
+ image=image,
+ env=[PYTHONUNBUFFERED_ENV]
+ )
+ environment_container = client.V1Container(
+ name="environment",
+ image=ENVIRONMENT_IMAGE,
+ env=[PYTHONUNBUFFERED_ENV, AUTH_TOKEN_ENV, DJANGO_SERVER_ENV, MESSAGE_BODY_ENV]
+ )
+ template = client.V1PodTemplateSpec(
+ metadata=client.V1ObjectMeta(labels={"app": "evaluation"}),
+ spec=client.V1PodSpec(containers=[environment_container, agent_container]))
+ spec = client.ExtensionsV1beta1DeploymentSpec(
+ replicas=1,
+ template=template)
+ deployment = client.ExtensionsV1beta1Deployment(
+ api_version="extensions/v1beta1",
+ kind="Deployment",
+ metadata=client.V1ObjectMeta(name="submission-{0}".format(submission)),
+ spec=spec)
+ return deployment
+
+
+def create_deployment(api_instance, deployment):
+ api_response = api_instance.create_namespaced_deployment(
+ body=deployment,
+ namespace="default")
+ logger.info("Deployment created. status='%s'" % str(api_response.status))
+
+
+def process_submission_callback(message, api):
+ config.load_kube_config()
+ extensions_v1beta1 = client.ExtensionsV1beta1Api()
+ logger.info(message)
+ submission_data = {
+ "submission_status": "running",
+ "submission": message["submission_pk"],
+ }
+ logger.info(submission_data)
+ api.update_submission_status(submission_data, message["challenge_pk"])
+ dep = create_deployment_object(
+ message["submitted_image_uri"],
+ message["submission_pk"],
+ message
+ )
+ create_deployment(extensions_v1beta1, dep)
+
+
+def main():
+ api = EvalAI_Interface(
+ AUTH_TOKEN=AUTH_TOKEN,
+ DJANGO_SERVER=DJANGO_SERVER,
+ DJANGO_SERVER_PORT=DJANGO_SERVER_PORT,
+ QUEUE_NAME=QUEUE_NAME,
+ )
+ logger.info("String RL Worker for {}".format(api.get_challenge_by_queue_name()["title"]))
+ killer = GracefulKiller()
+ while True:
+ logger.info(
+ "Fetching new messages from the queue {}".format(QUEUE_NAME)
+ )
+ message = api.get_message_from_sqs_queue()
+ logger.info(message)
+ message_body = message.get("body")
+ if message_body:
+ submission_pk = message_body.get("submission_pk")
+ submission = api.get_submission_by_pk(submission_pk)
+ if submission:
+ if submission.get("status") == "finished":
+ message_receipt_handle = message.get("receipt_handle")
+ api.delete_message_from_sqs_queue(message_receipt_handle)
+ elif submission.get("status") == "running":
+ continue
+ else:
+ message_receipt_handle = message.get("receipt_handle")
+ logger.info(
+ "Processing message body: {}".format(message_body)
+ )
+ process_submission_callback(message_body, api)
+ api.delete_message_from_sqs_queue(message.get("receipt_handle"))
+ time.sleep(MESSAGE_FETCH_DEPLAY)
+ if killer.kill_now:
+ break
+
+
+if __name__ == "__main__":
+ main()
+ logger.info("Quitting Submission Worker.")
diff --git a/scripts/workers/worker_util.py b/scripts/workers/worker_util.py
new file mode 100644
index 0000000000..1facc50e2a
--- /dev/null
+++ b/scripts/workers/worker_util.py
@@ -0,0 +1,104 @@
+import logging
+import requests
+
+logger = logging.getLogger(__name__)
+
+
+URLS = {
+ "get_message_from_sqs_queue": "/api/jobs/challenge/queues/{}/",
+ "delete_message_from_sqs_queue": "/api/jobs/queues/{}/receipt/{}/",
+ "get_submission_by_pk": "/api/jobs/submission/{}",
+ "get_challenge_phases_by_challenge_pk": "/api/challenges/{}/phases/",
+ "get_challenge_by_queue_name": "/api/challenges/challenge/queues/{}/",
+ "get_challenge_phase_by_pk": "/api/challenges/challenge/{}/challenge_phase/{}",
+ "update_submission_data": "/api/jobs/challenge/{}/update_submission/",
+}
+
+
+class EvalAI_Interface:
+
+ def __init__(
+ self,
+ AUTH_TOKEN,
+ DJANGO_SERVER,
+ DJANGO_SERVER_PORT,
+ QUEUE_NAME,
+
+ ):
+ self.AUTH_TOKEN = AUTH_TOKEN
+ self.DJANGO_SERVER = DJANGO_SERVER
+ self.DJANGO_SERVER_PORT = DJANGO_SERVER_PORT
+ self.QUEUE_NAME = QUEUE_NAME
+
+ def get_request_headers(self):
+ headers = {"Authorization": "Token {}".format(self.AUTH_TOKEN)}
+ return headers
+
+ def make_request(self, url, method, data=None):
+ headers = self.get_request_headers()
+ try:
+ response = requests.request(method=method, url=url, headers=headers)
+ response.raise_for_status()
+ except requests.exceptions.RequestException:
+ logger.info(
+ "The worker is not able to establish connection with EvalAI"
+ )
+ raise
+ return response.json()
+
+ def return_url_per_environment(self, url):
+ base_url = "{0}:{1}".format(self.DJANGO_SERVER, self.DJANGO_SERVER_PORT)
+ url = "{0}{1}".format(base_url, url)
+ return url
+
+ def get_message_from_sqs_queue(self):
+ url = URLS.get("get_message_from_sqs_queue").format(self.QUEUE_NAME)
+ url = self.return_url_per_environment(url)
+ response = self.make_request(url, "GET")
+ return response
+
+ def delete_message_from_sqs_queue(self, receipt_handle):
+ url = URLS.get("delete_message_from_sqs_queue").format(
+ self.QUEUE_NAME, receipt_handle
+ )
+ url = self.return_url_per_environment(url)
+ response = self.make_request(url, "GET") # noqa
+ return
+
+ def get_submission_by_pk(self, submission_pk):
+ url = URLS.get("get_submission_by_pk").format(submission_pk)
+ url = self.return_url_per_environment(url)
+ response = self.make_request(url, "GET")
+ return response
+
+ def get_challenge_phases_by_challenge_pk(self, challenge_pk):
+ url = URLS.get("get_challenge_phases_by_challenge_pk").format(challenge_pk)
+ url = self.return_url_per_environment(url)
+ response = self.make_request(url, "GET")
+ return response
+
+ def get_challenge_by_queue_name(self):
+ url = URLS.get("get_challenge_by_queue_name").format(self.QUEUE_NAME)
+ url = self.return_url_per_environment(url)
+ response = self.make_request(url, "GET")
+ return response
+
+ def get_challenge_phase_by_pk(self, challenge_pk, challenge_phase_pk):
+ url = URLS.get("get_challenge_phase_by_pk").format(
+ challenge_pk, challenge_phase_pk
+ )
+ url = self.return_url_per_environment(url)
+ response = self.make_request(url, "GET")
+ return response
+
+ def update_submission_data(self, data, challenge_pk, submission_pk):
+ url = URLS.get("update_submission_data").format(challenge_pk)
+ url = self.return_url_per_environment(url)
+ response = self.make_request(url, "PUT", data=data)
+ return response
+
+ def update_submission_status(self, data, challenge_pk):
+ url = "/api/jobs/challenge/{}/update_submission/".format(challenge_pk)
+ url = self.return_url_per_environment(url)
+ response = self.make_request(url, "PATCH", data=data)
+ return response
From 0ca1e1fcba393f7685995f863be64f222b28f184 Mon Sep 17 00:00:00 2001
From: Takitsuse Nagisa <57856193+takitsuse@users.noreply.github.com>
Date: Mon, 23 Dec 2019 05:03:51 +0900
Subject: [PATCH 11/34] Frontend: Fix typo and remove verbosity in challenge
controller(#2528)
* Remove verbosity
* Fix typo
Co-authored-by: Rishabh Jain
---
apps/hosts/models.py | 2 +-
frontend/src/js/controllers/challengeCtrl.js | 6 ++----
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/apps/hosts/models.py b/apps/hosts/models.py
index b58af8da54..d5fa3bd756 100644
--- a/apps/hosts/models.py
+++ b/apps/hosts/models.py
@@ -10,7 +10,7 @@
class ChallengeHostTeam(TimeStampedModel):
"""
- Model representing the Host Team for a partiuclar challenge
+ Model representing the Host Team for a particular challenge
"""
team_name = models.CharField(max_length=100, unique=True)
diff --git a/frontend/src/js/controllers/challengeCtrl.js b/frontend/src/js/controllers/challengeCtrl.js
index f7df68bb81..b7d2838ffa 100644
--- a/frontend/src/js/controllers/challengeCtrl.js
+++ b/frontend/src/js/controllers/challengeCtrl.js
@@ -1891,10 +1891,8 @@
vm.challengePhaseDialog = function(ev, phase) {
vm.page.challenge_phase = phase;
vm.page.max_submissions_per_day = phase.max_submissions_per_day;
- vm.phaseStartDate = phase.start_date;
- vm.phaseStartDate = moment(vm.phaseStartDate);
- vm.phaseEndDate = phase.end_date;
- vm.phaseEndDate = moment(vm.phaseEndDate);
+ vm.phaseStartDate = moment(phase.start_date);
+ vm.phaseEndDate = moment(phase.end_date);
vm.testAnnotationFile = null;
vm.sanityCheckPass = true;
vm.sanityCheck = "";
From ac59406427da6ab64436a14f556cad84e44826a2 Mon Sep 17 00:00:00 2001
From: Takitsuse Nagisa <57856193+takitsuse@users.noreply.github.com>
Date: Mon, 23 Dec 2019 05:10:08 +0900
Subject: [PATCH 12/34] Fix #1705: Add support for editing the challenge start
end end date(#2512)
* Add support for editing the challenge start and end date
* Throw error when the challenge start date is not less than the end date
* Correct a comment on vm.challengeDateDialog
* Add empty line on EOF
* Turn off autocomplete
* Fix indentations
* Add modal title
* Fix bug that appears when clicking cancel button
* Add margin
* Fix min-date of moment-picker
* Retry travis ci
* Display buttons on small devices
* Adjust the width of the modal
* Reflect the change of date immidiately
* Fix validation on vm.editChallengeDate
* Improve error message
* delete min-date
* Move sendRequest to proper line
* Trim empty lines
* Remove verbosity
Co-authored-by: Rishabh Jain
---
frontend/src/js/controllers/challengeCtrl.js | 57 +++++++++++++++++++
.../views/web/challenge/challenge-page.html | 22 +++++++
.../edit-challenge/edit-challenge-date.html | 43 ++++++++++++++
3 files changed, 122 insertions(+)
create mode 100644 frontend/src/views/web/challenge/edit-challenge/edit-challenge-date.html
diff --git a/frontend/src/js/controllers/challengeCtrl.js b/frontend/src/js/controllers/challengeCtrl.js
index b7d2838ffa..9bcb711561 100644
--- a/frontend/src/js/controllers/challengeCtrl.js
+++ b/frontend/src/js/controllers/challengeCtrl.js
@@ -2006,6 +2006,63 @@
});
};
+ // Edit Challenge Start and End Date
+ vm.challengeDateDialog = function(ev) {
+ vm.challengeStartDate = moment(vm.page.start_date);
+ vm.challengeEndDate = moment(vm.page.end_date);
+ $mdDialog.show({
+ scope: $scope,
+ preserveScope: true,
+ targetEvent: ev,
+ templateUrl: 'dist/views/web/challenge/edit-challenge/edit-challenge-date.html',
+ escapeToClose: false
+ });
+ };
+
+ vm.editChallengeDate = function(editChallengeDateForm) {
+ if (editChallengeDateForm) {
+ var challengeHostList = utilities.getData("challengeCreator");
+ for (var challenge in challengeHostList) {
+ if (challenge == vm.challengeId) {
+ vm.challengeHostId = challengeHostList[challenge];
+ break;
+ }
+ }
+ parameters.url = "challenges/challenge_host_team/" + vm.challengeHostId + "/challenge/" + vm.challengeId;
+ parameters.method = 'PATCH';
+ if (new Date(vm.challengeStartDate).valueOf() < new Date(vm.challengeEndDate).valueOf()) {
+ parameters.data = {
+ "start_date": vm.challengeStartDate,
+ "end_date": vm.challengeEndDate
+ };
+ parameters.callback = {
+ onSuccess: function(response) {
+ var status = response.status;
+ utilities.hideLoader();
+ if (status === 200) {
+ vm.page.start_date = vm.challengeStartDate.format("MMM D, YYYY h:mm:ss A");
+ vm.page.end_date = vm.challengeEndDate.format("MMM D, YYYY h:mm:ss A");
+ $mdDialog.hide();
+ $rootScope.notify("success", "The challenge start and end date is successfully updated!");
+ }
+ },
+ onError: function(response) {
+ utilities.hideLoader();
+ $mdDialog.hide();
+ var error = response.data;
+ $rootScope.notify("error", error);
+ }
+ };
+ utilities.showLoader();
+ utilities.sendRequest(parameters);
+ } else {
+ $rootScope.notify("error", "The challenge start date cannot be same or greater than end date.");
+ }
+ } else {
+ $mdDialog.hide();
+ }
+ };
+
$scope.$on('$destroy', function() {
vm.stopFetchingSubmissions();
vm.stopLeaderboard();
diff --git a/frontend/src/views/web/challenge/challenge-page.html b/frontend/src/views/web/challenge/challenge-page.html
index 24c292cb20..af6d857c6d 100644
--- a/frontend/src/views/web/challenge/challenge-page.html
+++ b/frontend/src/views/web/challenge/challenge-page.html
@@ -42,6 +42,28 @@
+
+
+ Starts on:
+ {{ challenge.page.start_date | date:'medium' }}
+
+
+
+
+
+
+
+
+
+ Ends on:
+ {{ challenge.page.end_date | date:'medium' }}
+
+
+
+
+
+
+
diff --git a/frontend/src/views/web/challenge/edit-challenge/edit-challenge-date.html b/frontend/src/views/web/challenge/edit-challenge/edit-challenge-date.html
new file mode 100644
index 0000000000..57c1a35bb6
--- /dev/null
+++ b/frontend/src/views/web/challenge/edit-challenge/edit-challenge-date.html
@@ -0,0 +1,43 @@
+
From f21866f8fa8ef86967f254f4fdaad44e476f9c92 Mon Sep 17 00:00:00 2001
From: Kartik Verma
Date: Tue, 24 Dec 2019 01:48:43 +0530
Subject: [PATCH 13/34] Remote Worker: Fix API endpoint to delete message from
SQS queue(#2429)
* Fix delete_submission_message_from_queue API
* Changed behaviour of remote submission worker to delete queue message using updated contract
* Resolve test for delete_message_from_sqs_queue_url
* Refactored code to fix unit tests
Co-authored-by: Rishabh Jain
---
apps/jobs/urls.py | 2 +-
apps/jobs/views.py | 7 ++---
scripts/workers/remote_submission_worker.py | 27 ++++++++++++++++---
tests/unit/remoteworker/test_remote_worker.py | 13 ++++++---
4 files changed, 37 insertions(+), 12 deletions(-)
diff --git a/apps/jobs/urls.py b/apps/jobs/urls.py
index 42f6dac14a..82418c0a2c 100644
--- a/apps/jobs/urls.py
+++ b/apps/jobs/urls.py
@@ -46,7 +46,7 @@
name="get_submissions_for_challenge",
),
url(
- r"^queues/(?P[\w-]+)/receipt/(?P[\w-]+)/$",
+ r"^queues/(?P[\w-]+)/$",
views.delete_submission_message_from_queue,
name="delete_submission_message_from_queue",
),
diff --git a/apps/jobs/views.py b/apps/jobs/views.py
index cc91fcd473..a2c7f99999 100644
--- a/apps/jobs/views.py
+++ b/apps/jobs/views.py
@@ -1129,11 +1129,11 @@ def get_submission_message_from_queue(request, queue_name):
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
-@api_view(["GET"])
+@api_view(["POST"])
@throttle_classes([UserRateThrottle])
@permission_classes((permissions.IsAuthenticated, HasVerifiedEmail))
@authentication_classes((ExpiringTokenAuthentication,))
-def delete_submission_message_from_queue(request, queue_name, receipt_handle):
+def delete_submission_message_from_queue(request, queue_name):
"""
API to delete submission message from AWS SQS queue
Arguments:
@@ -1151,13 +1151,14 @@ def delete_submission_message_from_queue(request, queue_name, receipt_handle):
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
challenge_pk = challenge.pk
+ receipt_handle = request.data["receipt_handle"]
if not is_user_a_host_of_challenge(request.user, challenge_pk):
response_data = {
"error": "Sorry, you are not authorized to access this resource"
}
return Response(response_data, status=status.HTTP_401_UNAUTHORIZED)
- queue = get_sqs_queue_object()
+ queue = get_sqs_queue_object(queue_name)
try:
message = queue.Message(receipt_handle)
message.delete()
diff --git a/scripts/workers/remote_submission_worker.py b/scripts/workers/remote_submission_worker.py
index c2ec074b7c..8a0789d8ca 100644
--- a/scripts/workers/remote_submission_worker.py
+++ b/scripts/workers/remote_submission_worker.py
@@ -44,7 +44,7 @@
EVALUATION_SCRIPTS = {}
URLS = {
"get_message_from_sqs_queue": "/api/jobs/challenge/queues/{}/",
- "delete_message_from_sqs_queue": "/api/jobs/queues/{}/receipt/{}/",
+ "delete_message_from_sqs_queue": "/api/jobs/queues/{}/",
"get_submission_by_pk": "/api/jobs/submission/{}",
"get_challenge_phases_by_challenge_pk": "/api/challenges/{}/phases/",
"get_challenge_by_queue_name": "/api/challenges/challenge/queues/{}/",
@@ -378,6 +378,23 @@ def make_request(url, method, data=None):
raise
return response.json()
+ elif method == "POST":
+ try:
+ response = requests.post(url=url, headers=headers, data=data)
+ response.raise_for_status()
+ except requests.exceptions.RequestException:
+ logger.info(
+ "The worker is not able to establish connection with EvalAI"
+ )
+ raise
+ except requests.exceptions.HTTPError:
+ logger.info(
+ "The request to URL {} is failed due to {}"
+ % (url, response.json())
+ )
+ raise
+ return response.json()
+
def get_message_from_sqs_queue():
url = URLS.get("get_message_from_sqs_queue").format(QUEUE_NAME)
@@ -388,11 +405,13 @@ def get_message_from_sqs_queue():
def delete_message_from_sqs_queue(receipt_handle):
url = URLS.get("delete_message_from_sqs_queue").format(
- QUEUE_NAME, receipt_handle
+ QUEUE_NAME
)
url = return_url_per_environment(url)
- response = make_request(url, "GET") # noqa
- return
+ response = make_request(url, "POST", data={
+ "receipt_handle": receipt_handle
+ }) # noqa
+ return response
def get_submission_by_pk(submission_pk):
diff --git a/tests/unit/remoteworker/test_remote_worker.py b/tests/unit/remoteworker/test_remote_worker.py
index c3e1de0dd2..c2f89fe02d 100644
--- a/tests/unit/remoteworker/test_remote_worker.py
+++ b/tests/unit/remoteworker/test_remote_worker.py
@@ -36,8 +36,8 @@ def make_request_url(self):
def get_message_from_sqs_queue_url(self, queue_name):
return "/api/jobs/challenge/queues/{}/".format(queue_name)
- def delete_message_from_sqs_queue_url(self, queue_name, receipt_handle):
- return "/api/jobs/queues/{}/receipt/{}/".format(queue_name, receipt_handle)
+ def delete_message_from_sqs_queue_url(self, queue_name):
+ return "/api/jobs/queues/{}/".format(queue_name)
def get_submission_by_pk_url(self, submission_pk):
return "/api/jobs/submission/{}".format(submission_pk)
@@ -74,6 +74,10 @@ def test_make_request_patch(self, mock_make_request):
make_request(self.url, "PATCH", data=self.data)
mock_make_request.patch.assert_called_with(url=self.url, headers=self.headers, data=self.data)
+ def test_make_request_post(self, mock_make_request):
+ make_request(self.url, "POST", data=self.data)
+ mock_make_request.post.assert_called_with(url=self.url, headers=self.headers, data=self.data)
+
@mock.patch("scripts.workers.remote_submission_worker.QUEUE_NAME", "evalai_submission_queue")
@mock.patch("scripts.workers.remote_submission_worker.return_url_per_environment")
@@ -89,11 +93,12 @@ def test_get_message_from_sqs_queue(self, mock_make_request, mock_url):
def test_delete_message_from_sqs_queue(self, mock_make_request, mock_url):
test_receipt_handle = "MbZj6wDWli+JvwwJaBV+3dcjk2YW2vA3+STFFljTM8tJJg6HRG6PYSasuWXPJB+Cw"
- url = self.delete_message_from_sqs_queue_url("evalai_submission_queue", test_receipt_handle)
+ url = self.delete_message_from_sqs_queue_url("evalai_submission_queue")
delete_message_from_sqs_queue(test_receipt_handle)
mock_url.assert_called_with(url)
url = mock_url(url)
- mock_make_request.assert_called_with(url, "GET")
+ expected_data = {"receipt_handle": "MbZj6wDWli+JvwwJaBV+3dcjk2YW2vA3+STFFljTM8tJJg6HRG6PYSasuWXPJB+Cw"}
+ mock_make_request.assert_called_with(url, "POST", data=expected_data)
def test_get_challenge_by_queue_name(self, mock_make_request, mock_url):
url = self.get_challenge_by_queue_name_url("evalai_submission_queue")
From dcbc4f4077568d8e5d3b2e048f5817ff1cbde68b Mon Sep 17 00:00:00 2001
From: Kartik Verma
Date: Tue, 24 Dec 2019 02:01:25 +0530
Subject: [PATCH 14/34] Backend: Add test environment URL field in phase
model(#2392)
* Added environment_url field in Phase
Signed-off-by: vkartik97 <3920286+vkartik97@users.noreply.github.com>
* Resolving Migrations conflicts
Signed-off-by: vkartik97 <3920286+vkartik97@users.noreply.github.com>
* Removing changes in seed script
* Resolve conflicts in migrations
* update error message when challenge isn't docker based
Co-authored-by: Rishabh Jain
---
...dding_environment_url_for_rl_challenges.py | 21 ++++++++++++
apps/challenges/models.py | 2 ++
apps/challenges/urls.py | 5 +++
apps/challenges/views.py | 32 +++++++++++++++++++
4 files changed, 60 insertions(+)
create mode 100644 apps/challenges/migrations/0058_adding_environment_url_for_rl_challenges.py
diff --git a/apps/challenges/migrations/0058_adding_environment_url_for_rl_challenges.py b/apps/challenges/migrations/0058_adding_environment_url_for_rl_challenges.py
new file mode 100644
index 0000000000..5b2872a00b
--- /dev/null
+++ b/apps/challenges/migrations/0058_adding_environment_url_for_rl_challenges.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-07-17 09:25
+from __future__ import unicode_literals
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('challenges', '0058_add_show_leaderboard_by_latest_submission_field_in_challenge_phase_split_model'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='challengephase',
+ name='environment_url',
+ field=models.CharField(max_length=2128, null=True, validators=[django.core.validators.URLValidator()]),
+ ),
+ ]
diff --git a/apps/challenges/models.py b/apps/challenges/models.py
index cfa3a4e22a..e1f952eea7 100644
--- a/apps/challenges/models.py
+++ b/apps/challenges/models.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
from django.contrib.auth.models import User
+from django.core.validators import URLValidator
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils import timezone
@@ -232,6 +233,7 @@ def __init__(self, *args, **kwargs):
null=True,
)
slug = models.SlugField(max_length=200, null=True, unique=True)
+ environment_url = models.CharField(validators=[URLValidator()], null=True, max_length=2128) # Max length of URL and tag is 2000 and 128 respectively
class Meta:
app_label = "challenges"
diff --git a/apps/challenges/urls.py b/apps/challenges/urls.py
index 61d9865bc1..3f44c5b8dd 100644
--- a/apps/challenges/urls.py
+++ b/apps/challenges/urls.py
@@ -151,4 +151,9 @@
views.get_challenge_phase_by_slug,
name="get_challenge_phase_by_slug",
),
+ url(
+ r"^phase/environment/(?P[\w-]+)/$",
+ views.get_challenge_phase_environment_url,
+ name="get_challenge_phase_environment_url",
+ ),
]
diff --git a/apps/challenges/views.py b/apps/challenges/views.py
index 2fb695c462..c75d064d47 100644
--- a/apps/challenges/views.py
+++ b/apps/challenges/views.py
@@ -2162,3 +2162,35 @@ def get_challenge_phase_by_slug(request, slug):
serializer = ChallengePhaseSerializer(challenge_phase)
response_data = serializer.data
return Response(response_data, status=status.HTTP_200_OK)
+
+
+@api_view(["GET"])
+@throttle_classes([UserRateThrottle])
+@permission_classes((permissions.IsAuthenticated, HasVerifiedEmail))
+@authentication_classes((ExpiringTokenAuthentication,))
+def get_challenge_phase_environment_url(request, slug):
+ """
+ Returns environment image url and tag required for RL challenge evaluation
+ """
+ try:
+ challenge_phase = ChallengePhase.objects.get(slug=slug)
+ except ChallengePhase.DoesNotExist:
+ response_data = {
+ "error": "Challenge phase with slug {} does not exist".format(slug)
+ }
+ return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
+ challenge = get_challenge_model(challenge_phase.challenge.pk)
+ if not is_user_a_host_of_challenge(request.user, challenge.pk):
+ response_data = {
+ "error": "Sorry, you are not authorized to access test environment URL."
+ }
+ return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
+ if not challenge.is_docker_based:
+ response_data = {
+ "error": "The challenge doesn't require uploading Docker images, hence no test environment URL."
+ }
+ return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
+ response_data = {
+ "environment_url": challenge_phase.environment_url
+ }
+ return Response(response_data, status=status.HTTP_200_OK)
From f5adee083d939cb31940685bbe396786983dbccf Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Tue, 24 Dec 2019 09:20:44 -0500
Subject: [PATCH 15/34] Requirements: Add kubernetes python-client(#2535)
---
requirements/common.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements/common.txt b/requirements/common.txt
index 78e039587c..14b88606f9 100644
--- a/requirements/common.txt
+++ b/requirements/common.txt
@@ -15,6 +15,7 @@ django-ses==0.8.5
docker-compose==1.21.0
drfdocs==0.0.11
drf-yasg==1.11.0
+kubernetes==10.0.1
moto==1.3.8
pika==0.10.0
pickleshare==0.7.4
From 4d495c51457c281b7009cbad5f93cbe495c987a4 Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Tue, 24 Dec 2019 12:34:01 -0500
Subject: [PATCH 16/34] Backend: Add blank=True in challenge phase environment
URL (#2536)
* Add blank=True for phase environment URL
* Add migrations file
---
...0059_add_blank_in_phase_environment_url.py | 26 +++++++++++++++++++
apps/challenges/models.py | 22 ++++++++++------
2 files changed, 40 insertions(+), 8 deletions(-)
create mode 100644 apps/challenges/migrations/0059_add_blank_in_phase_environment_url.py
diff --git a/apps/challenges/migrations/0059_add_blank_in_phase_environment_url.py b/apps/challenges/migrations/0059_add_blank_in_phase_environment_url.py
new file mode 100644
index 0000000000..2c111bf743
--- /dev/null
+++ b/apps/challenges/migrations/0059_add_blank_in_phase_environment_url.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.23 on 2019-12-24 17:28
+from __future__ import unicode_literals
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("challenges", "0058_adding_environment_url_for_rl_challenges")
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="challengephase",
+ name="environment_url",
+ field=models.CharField(
+ blank=True,
+ max_length=2128,
+ null=True,
+ validators=[django.core.validators.URLValidator()],
+ ),
+ )
+ ]
diff --git a/apps/challenges/models.py b/apps/challenges/models.py
index e1f952eea7..ff975755c3 100644
--- a/apps/challenges/models.py
+++ b/apps/challenges/models.py
@@ -85,7 +85,7 @@ def __init__(self, *args, **kwargs):
models.TextField(null=True, blank=True),
default=[],
blank=True,
- null=True
+ null=True,
)
remote_evaluation = models.BooleanField(
default=False, verbose_name="Remote Evaluation", db_index=True
@@ -123,11 +123,11 @@ def __init__(self, *args, **kwargs):
max_length=20, verbose_name="evalai-cli version", null=True, blank=True
)
# The number of active workers on Fargate of the challenge.
- workers = models.IntegerField(
- null=True, blank=True, default=None
- )
+ workers = models.IntegerField(null=True, blank=True, default=None)
# The task definition ARN for the challenge, used for updating and creating service.
- task_def_arn = models.CharField(null=True, blank=True, max_length=2048, default="")
+ task_def_arn = models.CharField(
+ null=True, blank=True, max_length=2048, default=""
+ )
class Meta:
app_label = "challenges"
@@ -171,7 +171,9 @@ def is_active(self):
weak=False,
)
signals.post_save.connect(
- model_field_name(field_name="evaluation_script")(restart_workers_signal_callback),
+ model_field_name(field_name="evaluation_script")(
+ restart_workers_signal_callback
+ ),
sender=Challenge,
weak=False,
)
@@ -233,7 +235,9 @@ def __init__(self, *args, **kwargs):
null=True,
)
slug = models.SlugField(max_length=200, null=True, unique=True)
- environment_url = models.CharField(validators=[URLValidator()], null=True, max_length=2128) # Max length of URL and tag is 2000 and 128 respectively
+ environment_url = models.CharField(
+ validators=[URLValidator()], null=True, blank=True, max_length=2128
+ ) # Max length of URL and tag is 2000 and 128 respectively
class Meta:
app_label = "challenges"
@@ -282,7 +286,9 @@ def save(self, *args, **kwargs):
weak=False,
)
signals.post_save.connect(
- model_field_name(field_name="test_annotation")(restart_workers_signal_callback),
+ model_field_name(field_name="test_annotation")(
+ restart_workers_signal_callback
+ ),
sender=ChallengePhase,
weak=False,
)
From 9fd7cba86ad95b9c80d318a8e9a2fc410bf229bd Mon Sep 17 00:00:00 2001
From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com>
Date: Thu, 26 Dec 2019 23:19:57 +0530
Subject: [PATCH 17/34] Worker: Refactor code and add tests for
download_and_extract_zip_file function(#2532)
* Refactored download_and_extract_zip_file
* Correct names
* Fix flake8 issues
* Add tests for extract_zip_file and delete_zip_file
* Fix import issue
* Remove unused imports
* Revert "Remove unused imports"
This reverts commit 0bb5423d7d5b87cc6b04a03b9feb2bac0578000b.
* Merge with PR#2531
* Remove unrequired changes
---
scripts/workers/submission_worker.py | 43 ++++++++---
tests/unit/worker/test_submission_worker.py | 84 +++++++++++++++++++++
2 files changed, 115 insertions(+), 12 deletions(-)
diff --git a/scripts/workers/submission_worker.py b/scripts/workers/submission_worker.py
index 7b0f501014..09001203f8 100644
--- a/scripts/workers/submission_worker.py
+++ b/scripts/workers/submission_worker.py
@@ -130,6 +130,35 @@ def download_and_extract_file(url, download_location):
f.write(chunk)
+def extract_zip_file(download_location, extract_location):
+ """
+ Helper function to extract zip file
+ Params:
+ * `download_location`: Location of zip file
+ * `extract_location`: Location of directory for extracted file
+ """
+ zip_ref = zipfile.ZipFile(download_location, "r")
+ zip_ref.extractall(extract_location)
+ zip_ref.close()
+
+
+def delete_zip_file(download_location):
+ """
+ Helper function to remove zip file from location `download_location`
+ Params:
+ * `download_location`: Location of file to be removed.
+ """
+ try:
+ os.remove(download_location)
+ except Exception as e:
+ logger.error(
+ "Failed to remove zip file {}, error {}".format(
+ download_location, e
+ )
+ )
+ traceback.print_exc()
+
+
def download_and_extract_zip_file(url, download_location, extract_location):
"""
* Function to extract download a zip file, extract it and then removes the zip file.
@@ -147,19 +176,9 @@ def download_and_extract_zip_file(url, download_location, extract_location):
if chunk:
f.write(chunk)
# extract zip file
- zip_ref = zipfile.ZipFile(download_location, "r")
- zip_ref.extractall(extract_location)
- zip_ref.close()
+ extract_zip_file(download_location, extract_location)
# delete zip file
- try:
- os.remove(download_location)
- except Exception as e:
- logger.error(
- "Failed to remove zip file {}, error {}".format(
- download_location, e
- )
- )
- traceback.print_exc()
+ delete_zip_file(download_location)
def create_dir(directory):
diff --git a/tests/unit/worker/test_submission_worker.py b/tests/unit/worker/test_submission_worker.py
index 166f3a1e7f..ef499420f7 100644
--- a/tests/unit/worker/test_submission_worker.py
+++ b/tests/unit/worker/test_submission_worker.py
@@ -1,11 +1,14 @@
import boto3
import mock
import os
+import responses
import shutil
import tempfile
+import zipfile
from datetime import timedelta
from moto import mock_sqs
+from io import BytesIO
from os.path import join
from django.contrib.auth.models import User
@@ -24,6 +27,9 @@
from scripts.workers.submission_worker import (
create_dir,
create_dir_as_python_package,
+ delete_zip_file,
+ download_and_extract_zip_file,
+ extract_zip_file,
extract_submission_data,
load_challenge_and_return_max_submissions,
return_file_url_per_environment,
@@ -220,3 +226,81 @@ def test_get_or_create_sqs_queue_for_non_existing_queue(self):
queue_url = self.sqs_client.get_queue_url(QueueName='test_queue_2')['QueueUrl']
self.assertTrue(queue_url)
self.sqs_client.delete_queue(QueueUrl=queue_url)
+
+
+class DownloadAndExtractZipFileTest(BaseAPITestClass):
+ def setUp(self):
+ super(DownloadAndExtractZipFileTest, self).setUp()
+ self.zip_name = "test"
+ self.req_url = "{}/{}".format(self.testserver, self.zip_name)
+ self.extract_location = join(self.BASE_TEMP_DIR, "test-dir")
+ self.download_location = join(self.extract_location, "{}.zip".format(self.zip_name))
+ create_dir(self.extract_location)
+
+ self.file_name = "test_file.txt"
+ self.file_content = b"file_content"
+
+ self.zip_file = BytesIO()
+ with zipfile.ZipFile(self.zip_file, mode="w", compression=zipfile.ZIP_DEFLATED) as zipper:
+ zipper.writestr(self.file_name, self.file_content)
+
+ def tearDown(self):
+ if os.path.exists(self.extract_location):
+ shutil.rmtree(self.extract_location)
+
+ @responses.activate
+ @mock.patch("scripts.workers.submission_worker.delete_zip_file")
+ @mock.patch("scripts.workers.submission_worker.extract_zip_file")
+ def test_download_and_extract_zip_file_success(self, mock_extract_zip, mock_delete_zip):
+ responses.add(
+ responses.GET, self.req_url,
+ content_type="application/zip",
+ body=self.zip_file.getvalue(), status=200)
+
+ download_and_extract_zip_file(self.req_url, self.download_location, self.extract_location)
+
+ with open(self.download_location, "rb") as downloaded:
+ self.assertEqual(downloaded.read(), self.zip_file.getvalue())
+ mock_extract_zip.assert_called_with(self.download_location, self.extract_location)
+ mock_delete_zip.assert_called_with(self.download_location)
+
+ @responses.activate
+ @mock.patch("scripts.workers.submission_worker.logger.error")
+ def test_download_and_extract_zip_file_when_download_fails(self, mock_logger):
+ e = "Error description"
+ responses.add(
+ responses.GET, self.req_url,
+ body=Exception(e))
+ error_message = "Failed to fetch file from {}, error {}".format(self.req_url, e)
+
+ download_and_extract_zip_file(self.req_url, self.download_location, self.extract_location)
+
+ mock_logger.assert_called_with(error_message)
+
+ def test_extract_zip_file(self):
+ with open(self.download_location, "wb") as zf:
+ zf.write(self.zip_file.getvalue())
+
+ extract_zip_file(self.download_location, self.extract_location)
+ extracted_path = join(self.extract_location, self.file_name)
+ self.assertTrue(os.path.exists(extracted_path))
+ with open(extracted_path, "rb") as extracted:
+ self.assertEqual(extracted.read(), self.file_content)
+
+ def test_delete_zip_file(self):
+ with open(self.download_location, "wb") as zf:
+ zf.write(self.zip_file.getvalue())
+
+ delete_zip_file(self.download_location)
+
+ self.assertFalse(os.path.exists(self.download_location))
+
+ @mock.patch("scripts.workers.submission_worker.logger.error")
+ @mock.patch("scripts.workers.submission_worker.os.remove")
+ def test_delete_zip_file_error(self, mock_remove, mock_logger):
+ e = "Error description"
+ mock_remove.side_effect = Exception(e)
+ error_message = "Failed to remove zip file {}, error {}".format(self.download_location, e)
+
+ delete_zip_file(self.download_location)
+ mock_logger.assert_called_with(error_message)
From 7961addc9e6717ce962ea74e275b6d1070f6a601 Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Thu, 26 Dec 2019 17:30:47 -0500
Subject: [PATCH 18/34] RL Worker: Minor code and indentation fixes(#2548)
---
scripts/workers/rl_submission_worker.py | 77 ++++++++++++-------------
scripts/workers/worker_util.py | 25 ++++----
2 files changed, 48 insertions(+), 54 deletions(-)
diff --git a/scripts/workers/rl_submission_worker.py b/scripts/workers/rl_submission_worker.py
index 70203c899c..6089c1ab8b 100644
--- a/scripts/workers/rl_submission_worker.py
+++ b/scripts/workers/rl_submission_worker.py
@@ -3,9 +3,7 @@
import signal
import time
-from .worker_util import (
- EvalAI_Interface
-)
+from worker_util import EvalAI_Interface
from kubernetes import client, config
@@ -23,59 +21,57 @@ def exit_gracefully(self, signum, frame):
logger = logging.getLogger(__name__)
-AUTH_TOKEN = os.environ.get("AUTH_TOKEN", "x")
-DJANGO_SERVER = os.environ.get("DJANGO_SERVER", "http://localhost")
-DJANGO_SERVER_PORT = os.environ.get("DJANGO_SERVER_PORT", "8000")
+AUTH_TOKEN = os.environ.get("AUTH_TOKEN", "auth_token")
+EVALAI_API_SERVER = os.environ.get(
+ "EVALAI_API_SERVER", "http://localhost:8000"
+)
QUEUE_NAME = os.environ.get("QUEUE_NAME", "evalai_submission_queue")
-ENVIRONMENT_IMAGE = os.environ.get("ENVIRONMENT_IMAGE", "x:tag")
+ENVIRONMENT_IMAGE = os.environ.get("ENVIRONMENT_IMAGE", "image_name:tag")
MESSAGE_FETCH_DEPLAY = int(os.environ.get("MESSAGE_FETCH_DEPLAY", "5"))
def create_deployment_object(image, submission, message):
- PYTHONUNBUFFERED_ENV = client.V1EnvVar(
- name="PYTHONUNBUFFERED",
- value="1",
- )
- AUTH_TOKEN_ENV = client.V1EnvVar(
- name="AUTH_TOKEN",
- value=AUTH_TOKEN
- )
- DJANGO_SERVER_ENV = client.V1EnvVar(
- name="DJANGO_SERVER",
- value=DJANGO_SERVER
- )
- MESSAGE_BODY_ENV = client.V1EnvVar(
- name="BODY",
- value=str(message)
+ PYTHONUNBUFFERED_ENV = client.V1EnvVar(name="PYTHONUNBUFFERED", value="1")
+ AUTH_TOKEN_ENV = client.V1EnvVar(name="AUTH_TOKEN", value=AUTH_TOKEN)
+ EVALAI_API_SERVER_ENV = client.V1EnvVar(
+ name="EVALAI_API_SERVER", value=EVALAI_API_SERVER
)
+ MESSAGE_BODY_ENV = client.V1EnvVar(name="BODY", value=str(message))
agent_container = client.V1Container(
- name="agent",
- image=image,
- env=[PYTHONUNBUFFERED_ENV]
+ name="agent", image=image, env=[PYTHONUNBUFFERED_ENV]
)
environment_container = client.V1Container(
name="environment",
image=ENVIRONMENT_IMAGE,
- env=[PYTHONUNBUFFERED_ENV, AUTH_TOKEN_ENV, DJANGO_SERVER_ENV, MESSAGE_BODY_ENV]
+ env=[
+ PYTHONUNBUFFERED_ENV,
+ AUTH_TOKEN_ENV,
+ EVALAI_API_SERVER_ENV,
+ MESSAGE_BODY_ENV,
+ ],
)
template = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(labels={"app": "evaluation"}),
- spec=client.V1PodSpec(containers=[environment_container, agent_container]))
+ spec=client.V1PodSpec(
+ containers=[environment_container, agent_container]
+ ),
+ )
spec = client.ExtensionsV1beta1DeploymentSpec(
- replicas=1,
- template=template)
+ replicas=1, template=template
+ )
deployment = client.ExtensionsV1beta1Deployment(
api_version="extensions/v1beta1",
kind="Deployment",
metadata=client.V1ObjectMeta(name="submission-{0}".format(submission)),
- spec=spec)
+ spec=spec,
+ )
return deployment
def create_deployment(api_instance, deployment):
api_response = api_instance.create_namespaced_deployment(
- body=deployment,
- namespace="default")
+ body=deployment, namespace="default"
+ )
logger.info("Deployment created. status='%s'" % str(api_response.status))
@@ -90,9 +86,7 @@ def process_submission_callback(message, api):
logger.info(submission_data)
api.update_submission_status(submission_data, message["challenge_pk"])
dep = create_deployment_object(
- message["submitted_image_uri"],
- message["submission_pk"],
- message
+ message["submitted_image_uri"], message["submission_pk"], message
)
create_deployment(extensions_v1beta1, dep)
@@ -100,11 +94,14 @@ def process_submission_callback(message, api):
def main():
api = EvalAI_Interface(
AUTH_TOKEN=AUTH_TOKEN,
- DJANGO_SERVER=DJANGO_SERVER,
- DJANGO_SERVER_PORT=DJANGO_SERVER_PORT,
+ EVALAI_API_SERVER=EVALAI_API_SERVER,
QUEUE_NAME=QUEUE_NAME,
)
- logger.info("String RL Worker for {}".format(api.get_challenge_by_queue_name()["title"]))
+ logger.info(
+ "String RL Worker for {}".format(
+ api.get_challenge_by_queue_name()["title"]
+ )
+ )
killer = GracefulKiller()
while True:
logger.info(
@@ -128,7 +125,9 @@ def main():
"Processing message body: {}".format(message_body)
)
process_submission_callback(message_body, api)
- api.delete_message_from_sqs_queue(message.get("receipt_handle"))
+ api.delete_message_from_sqs_queue(
+ message.get("receipt_handle")
+ )
time.sleep(MESSAGE_FETCH_DEPLAY)
if killer.kill_now:
break
diff --git a/scripts/workers/worker_util.py b/scripts/workers/worker_util.py
index 1facc50e2a..d31846e9a1 100644
--- a/scripts/workers/worker_util.py
+++ b/scripts/workers/worker_util.py
@@ -16,18 +16,9 @@
class EvalAI_Interface:
-
- def __init__(
- self,
- AUTH_TOKEN,
- DJANGO_SERVER,
- DJANGO_SERVER_PORT,
- QUEUE_NAME,
-
- ):
+ def __init__(self, AUTH_TOKEN, EVALAI_API_SERVER, QUEUE_NAME):
self.AUTH_TOKEN = AUTH_TOKEN
- self.DJANGO_SERVER = DJANGO_SERVER
- self.DJANGO_SERVER_PORT = DJANGO_SERVER_PORT
+ self.EVALAI_API_SERVER = EVALAI_API_SERVER
self.QUEUE_NAME = QUEUE_NAME
def get_request_headers(self):
@@ -37,7 +28,9 @@ def get_request_headers(self):
def make_request(self, url, method, data=None):
headers = self.get_request_headers()
try:
- response = requests.request(method=method, url=url, headers=headers)
+ response = requests.request(
+ method=method, url=url, headers=headers, data=data
+ )
response.raise_for_status()
except requests.exceptions.RequestException:
logger.info(
@@ -47,7 +40,7 @@ def make_request(self, url, method, data=None):
return response.json()
def return_url_per_environment(self, url):
- base_url = "{0}:{1}".format(self.DJANGO_SERVER, self.DJANGO_SERVER_PORT)
+ base_url = "{0}".format(self.EVALAI_API_SERVER)
url = "{0}{1}".format(base_url, url)
return url
@@ -63,7 +56,7 @@ def delete_message_from_sqs_queue(self, receipt_handle):
)
url = self.return_url_per_environment(url)
response = self.make_request(url, "GET") # noqa
- return
+ return response.status_code
def get_submission_by_pk(self, submission_pk):
url = URLS.get("get_submission_by_pk").format(submission_pk)
@@ -72,7 +65,9 @@ def get_submission_by_pk(self, submission_pk):
return response
def get_challenge_phases_by_challenge_pk(self, challenge_pk):
- url = URLS.get("get_challenge_phases_by_challenge_pk").format(challenge_pk)
+ url = URLS.get("get_challenge_phases_by_challenge_pk").format(
+ challenge_pk
+ )
url = self.return_url_per_environment(url)
response = self.make_request(url, "GET")
return response
From b95877c37eb93a1671c476eb9f4d86fb3c657739 Mon Sep 17 00:00:00 2001
From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com>
Date: Fri, 27 Dec 2019 22:46:39 +0530
Subject: [PATCH 19/34] Worker: Add tests for download_and_extract_file
function(#2545)
* Worker: Add test for download_and_extract_file
* Fix order of responses args
---
tests/unit/worker/test_submission_worker.py | 40 +++++++++++++++++++++
1 file changed, 40 insertions(+)
diff --git a/tests/unit/worker/test_submission_worker.py b/tests/unit/worker/test_submission_worker.py
index ef499420f7..a9dfe8b5d7 100644
--- a/tests/unit/worker/test_submission_worker.py
+++ b/tests/unit/worker/test_submission_worker.py
@@ -27,6 +27,7 @@
from scripts.workers.submission_worker import (
create_dir,
create_dir_as_python_package,
+ download_and_extract_file,
delete_zip_file,
download_and_extract_zip_file,
extract_zip_file,
@@ -228,6 +229,45 @@ def test_get_or_create_sqs_queue_for_non_existing_queue(self):
self.sqs_client.delete_queue(QueueUrl=queue_url)
+class DownloadAndExtractFileTest(BaseAPITestClass):
+ def setUp(self):
+ super(DownloadAndExtractFileTest, self).setUp()
+ self.req_url = "{}{}".format(self.testserver, self.url)
+ self.file_content = b"file content"
+
+ create_dir(self.temp_directory)
+ self.download_location = join(self.temp_directory, "dummy_file")
+
+ def tearDown(self):
+ if os.path.exists(self.temp_directory):
+ shutil.rmtree(self.temp_directory)
+
+ @responses.activate
+ def test_download_and_extract_file_success(self):
+ responses.add(responses.GET, self.req_url,
+ body=self.file_content,
+ content_type='application/octet-stream',
+ status=200)
+
+ download_and_extract_file(self.req_url, self.download_location)
+
+ self.assertTrue(os.path.exists(self.download_location))
+ with open(self.download_location, "rb") as f:
+ self.assertEqual(f.read(), self.file_content)
+
+ @responses.activate
+ @mock.patch("scripts.workers.submission_worker.logger.error")
+ def test_download_and_extract_file_when_download_fails(self, mock_logger):
+ error = "ExampleError: Example Error description"
+ responses.add(responses.GET, self.req_url, body=Exception(error))
+ expected = "Failed to fetch file from {}, error {}".format(self.req_url, error)
+
+ download_and_extract_file(self.req_url, self.download_location)
+
+ mock_logger.assert_called_with(expected)
+ self.assertFalse(os.path.exists(self.download_location))
+
+
class DownloadAndExtractZipFileTest(BaseAPITestClass):
def setUp(self):
super(DownloadAndExtractZipFileTest, self).setUp()
From 8de2555259a2ee6a71efb9d7cbec03bdfa9f8bbb Mon Sep 17 00:00:00 2001
From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com>
Date: Fri, 27 Dec 2019 23:36:28 +0530
Subject: [PATCH 20/34] Remote Worker: Add tests for download_and_extract_file
function(#2550)
* Add test for download_and_extract_file for remote worker
* Fix missing import
* Fix undefined variable
---
tests/unit/remoteworker/test_remote_worker.py | 42 +++++++++++++++++++
1 file changed, 42 insertions(+)
diff --git a/tests/unit/remoteworker/test_remote_worker.py b/tests/unit/remoteworker/test_remote_worker.py
index c2f89fe02d..98296478e2 100644
--- a/tests/unit/remoteworker/test_remote_worker.py
+++ b/tests/unit/remoteworker/test_remote_worker.py
@@ -1,5 +1,6 @@
import mock
import os
+import responses
import shutil
import tempfile
@@ -12,6 +13,7 @@
make_request,
get_message_from_sqs_queue,
delete_message_from_sqs_queue,
+ download_and_extract_file,
get_submission_by_pk,
get_challenge_phases_by_challenge_pk,
get_challenge_by_queue_name,
@@ -29,6 +31,7 @@ def setUp(self):
self.challenge_phase_pk = 1
self.data = {"test": "data"}
self.headers = {"Authorization": "Token test_token"}
+ self.testserver = "http://testserver"
def make_request_url(self):
return "/test/url"
@@ -170,3 +173,42 @@ def test_create_dir_as_python_package(self):
shutil.rmtree(self.temp_directory)
self.assertFalse(os.path.exists(self.temp_directory))
+
+
+class DownloadAndExtractFileTest(BaseTestClass):
+ def setUp(self):
+ super(DownloadAndExtractFileTest, self).setUp()
+ self.req_url = "{}{}".format(self.testserver, self.make_request_url())
+ self.file_content = b'file content'
+
+ self.temp_directory = tempfile.mkdtemp()
+ self.download_location = join(self.temp_directory, "dummy_file")
+
+ def tearDown(self):
+ if os.path.exists(self.temp_directory):
+ shutil.rmtree(self.temp_directory)
+
+ @responses.activate
+ def test_download_and_extract_file_success(self):
+ responses.add(responses.GET, self.req_url,
+ body=self.file_content,
+ content_type='application/octet-stream',
+ status=200)
+
+ download_and_extract_file(self.req_url, self.download_location)
+
+ self.assertTrue(os.path.exists(self.download_location))
+ with open(self.download_location, "rb") as f:
+ self.assertEqual(f.read(), self.file_content)
+
+ @responses.activate
+ @mock.patch("scripts.workers.remote_submission_worker.logger.error")
+ def test_download_and_extract_file_when_download_fails(self, mock_logger):
+ error = "ExampleError: Example Error description"
+ responses.add(responses.GET, self.req_url, body=Exception(error))
+ expected = "Failed to fetch file from {}, error {}".format(self.req_url, error)
+
+ download_and_extract_file(self.req_url, self.download_location)
+
+ mock_logger.assert_called_with(expected)
+ self.assertFalse(os.path.exists(self.download_location))
From 8680bcf102411a576bb891f15151b98bed58374b Mon Sep 17 00:00:00 2001
From: jayy <35180217+nsjcorps@users.noreply.github.com>
Date: Sat, 28 Dec 2019 18:44:28 +0100
Subject: [PATCH 21/34] Frontend: Add check for editing challenge start/end
date by challenge host(#2552)
---
frontend/src/views/web/challenge/challenge-page.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/views/web/challenge/challenge-page.html b/frontend/src/views/web/challenge/challenge-page.html
index af6d857c6d..8e6d025e92 100644
--- a/frontend/src/views/web/challenge/challenge-page.html
+++ b/frontend/src/views/web/challenge/challenge-page.html
@@ -48,7 +48,7 @@
{{ challenge.page.start_date | date:'medium' }}
-
+
@@ -59,7 +59,7 @@
{{ challenge.page.end_date | date:'medium' }}
-
+
From 3169c7fb3cecbdca17a3a92b8d7286d84a462769 Mon Sep 17 00:00:00 2001
From: Kartik Verma
Date: Sun, 29 Dec 2019 01:26:43 +0530
Subject: [PATCH 22/34] RL Submission Worker: Add submitted_image_uri as a SQS
message parameter for docker based challenges(#2376)
* Script to configure Kube Config for EKS.
Signed-off-by: Kartik Verma
* API to handle Docker Based Submissions
Signed-off-by: Kartik Verma
* Modified tests for modified endpoint
Signed-off-by: Kartik Verma
* Fixed Tests
Signed-off-by: Kartik Verma
* Fix admin job submission test
Signed-off-by: Kartik Verma
* Fixing Flake8 Error
Signed-off-by: Kartik Verma
* EvalAI API Interface for Workers
Signed-off-by: Kartik Verma
* Delete redendent script
* Delete redundent util
* Add dertails for message dict in comments
* Add details for submitted_image_uri key in message
* Initialize message
* Express intent clearly for usage for submitted_image_uri
* Fix wrong assignment of variable
* Add task to run submission for docker based challenge
* Fix linting errors
* Add behavior to re run docker based submission
---
apps/jobs/admin.py | 24 +++++++++++++++----
apps/jobs/sender.py | 19 +++++++---------
apps/jobs/tasks.py | 6 ++++-
apps/jobs/views.py | 43 +++++++++++++++++++++++++++++++----
tests/unit/jobs/test_views.py | 6 ++++-
5 files changed, 76 insertions(+), 22 deletions(-)
diff --git a/apps/jobs/admin.py b/apps/jobs/admin.py
index 3a8e046d31..7bc5c7fcac 100644
--- a/apps/jobs/admin.py
+++ b/apps/jobs/admin.py
@@ -1,13 +1,13 @@
import logging
+import requests
-from django.contrib import admin
+from django.contrib import admin, messages
from base.admin import ImportExportTimeStampedAdmin
from .models import Submission
from .sender import publish_submission_message
-
logger = logging.getLogger(__name__)
@@ -69,9 +69,23 @@ def submit_job_to_worker(self, request, queryset):
challenge_id, challenge_phase_id, submission_id
)
)
- publish_submission_message(
- challenge_id, challenge_phase_id, submission.id
- )
+ message = {
+ "challenge_pk": challenge_id,
+ "phase_pk": challenge_phase_id,
+ "submission_pk": submission.id
+ }
+
+ if submission.challenge_phase.challenge.is_docker_based:
+ try:
+ response = requests.get(submission.input_file)
+ except Exception as e:
+ messages.error(request, "Failed to get input_file with exception: {0}".format(e))
+ return
+
+ if response and response.status_code == 200:
+ message["submitted_image_uri"] = response.json()["submitted_image_uri"]
+
+ publish_submission_message(message)
queryset.update(status=Submission.SUBMITTED)
submit_job_to_worker.short_description = "Run selected submissions"
diff --git a/apps/jobs/sender.py b/apps/jobs/sender.py
index 2a1aaf12b1..0747b4a4f7 100644
--- a/apps/jobs/sender.py
+++ b/apps/jobs/sender.py
@@ -55,27 +55,24 @@ def get_or_create_sqs_queue(queue_name):
return queue
-def publish_submission_message(challenge_pk, phase_pk, submission_pk):
+def publish_submission_message(message):
"""
Args:
- challenge_pk: Challenge Id
- phase_pk: Challenge Phase Id
- submission_pk: Submission Id
+ message: A Dict with following keys
+ - "challenge_pk": int
+ - "phase_pk": int
+ - "submission_pk": int
+ - "submitted_image_uri": str, (only available when the challenge is a code upload challenge)
Returns:
Returns SQS response
"""
- message = {
- "challenge_pk": challenge_pk,
- "phase_pk": phase_pk,
- "submission_pk": submission_pk,
- }
try:
- challenge = Challenge.objects.get(pk=challenge_pk)
+ challenge = Challenge.objects.get(pk=message["challenge_pk"])
except Challenge.DoesNotExist:
logger.exception(
- "Challenge does not exist for the given id {}".format(challenge_pk)
+ "Challenge does not exist for the given id {}".format(message["challenge_pk"])
)
return
queue_name = challenge.queue
diff --git a/apps/jobs/tasks.py b/apps/jobs/tasks.py
index f57fd4d23f..6f3db2ea93 100644
--- a/apps/jobs/tasks.py
+++ b/apps/jobs/tasks.py
@@ -73,7 +73,11 @@ def download_file_and_publish_submission_message(
submission = serializer.instance
# publish messages in the submission worker queue
- publish_submission_message(challenge_phase.challenge.pk, challenge_phase.pk, submission.pk)
+ publish_submission_message({
+ "challenge_pk": challenge_phase.challenge.pk,
+ "phase_pk": challenge_phase.pk,
+ "submission_pk": submission.pk
+ })
logger.info("Message published to submission worker successfully!")
shutil.rmtree(downloaded_file['temp_dir_path'])
except Exception as e:
diff --git a/apps/jobs/views.py b/apps/jobs/views.py
index a2c7f99999..d862796dfa 100644
--- a/apps/jobs/views.py
+++ b/apps/jobs/views.py
@@ -3,6 +3,7 @@
import json
import logging
+import requests
from rest_framework import permissions, status
from rest_framework.decorators import (
api_view,
@@ -276,14 +277,30 @@ def challenge_submission(request, challenge_id, challenge_phase_id):
"request": request,
},
)
+ message = {
+ "challenge_pk": challenge_id,
+ "phase_pk": challenge_phase_id
+ }
+ if challenge.is_docker_based:
+ try:
+ file_content = json.loads(
+ request.FILES['input_file'].read()
+ )
+ message["submitted_image_uri"] = file_content["submitted_image_uri"]
+ except:
+ response_data = {
+ "error": "Error in deserializing submitted_image_uri from submission file"
+ }
+ return Response(
+ response_data, status=status.HTTP_400_BAD_REQUEST
+ )
if serializer.is_valid():
serializer.save()
response_data = serializer.data
submission = serializer.instance
+ message["submission_pk"] = submission.id
# publish message in the queue
- publish_submission_message(
- challenge_id, challenge_phase_id, submission.id
- )
+ publish_submission_message(message)
return Response(response_data, status=status.HTTP_201_CREATED)
return Response(
serializer.errors, status=status.HTTP_406_NOT_ACCEPTABLE
@@ -1027,7 +1044,25 @@ def re_run_submission(request, submission_pk):
}
return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE)
- publish_submission_message(challenge.pk, challenge_phase.pk, submission.pk)
+ message = {
+ "challenge_pk": challenge.pk,
+ "phase_pk": challenge_phase.pk,
+ "submission_pk": submission.pk
+ }
+
+ if submission.challenge_phase.challenge.is_docker_based:
+ try:
+ response = requests.get(submission.input_file)
+ except Exception as e:
+ response_data = {
+ "error": "Failed to get submission input file with error: {0}".format(e)
+ }
+ return Response(response_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ if response and response.status_code == 200:
+ message["submitted_image_uri"] = response.json()["submitted_image_uri"]
+
+ publish_submission_message(message)
response_data = {
"success": "Submission is successfully submitted for re-running"
}
diff --git a/tests/unit/jobs/test_views.py b/tests/unit/jobs/test_views.py
index bd47fd3f45..9645c0bd46 100644
--- a/tests/unit/jobs/test_views.py
+++ b/tests/unit/jobs/test_views.py
@@ -171,6 +171,10 @@ def setUp(self):
"dummy_input.txt", b"file_content", content_type="text/plain"
)
+ self.rl_submission_file = SimpleUploadedFile(
+ "submission.json", b'{"submitted_image_uri": "evalai-repo.com"}',
+ )
+
def tearDown(self):
shutil.rmtree("/tmp/evalai")
@@ -500,7 +504,7 @@ def test_challenge_submission_for_docker_based_challenges(self):
response = self.client.post(
self.url,
- {"status": "submitting", "input_file": self.input_file},
+ {"status": "submitting", "input_file": self.rl_submission_file},
format="multipart",
)
From 0dcc0026fb606cc41fbdab00b683c250e2380050 Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Sun, 29 Dec 2019 19:31:29 -0500
Subject: [PATCH 23/34] RL submission worker: Fix API's, get or create SQS
queue and re-running submission from admin (#2558)
* API fixes for RL submission worker
* Change name for SQS queue
* Fix submission re-run from admin for docker based challenges
---
apps/base/utils.py | 16 +++++++--------
apps/jobs/admin.py | 15 ++++++++++----
apps/jobs/views.py | 36 +++++++++++++++++++++-------------
scripts/workers/worker_util.py | 9 ++++-----
4 files changed, 44 insertions(+), 32 deletions(-)
diff --git a/apps/base/utils.py b/apps/base/utils.py
index f54c1de14f..14bad1b0d1 100644
--- a/apps/base/utils.py
+++ b/apps/base/utils.py
@@ -154,7 +154,7 @@ def get_boto3_client(resource, aws_keys):
logger.exception(e)
-def get_sqs_queue_object():
+def get_or_create_sqs_queue_object(queue_name):
if settings.DEBUG or settings.TEST:
queue_name = "evalai_submission_queue"
sqs = boto3.resource(
@@ -212,21 +212,18 @@ def send_slack_notification(webhook=settings.SLACK_WEB_HOOK_URL, message=""):
try:
data = {
"text": message["text"],
- "attachments": [
- {
- "color": "ffaf4b",
- "fields": message["fields"]
- }
- ]
+ "attachments": [{"color": "ffaf4b", "fields": message["fields"]}],
}
return requests.post(
webhook,
data=json.dumps(data),
- headers={"Content-Type": "application/json"}
+ headers={"Content-Type": "application/json"},
)
except Exception as e:
logger.exception(
- "Exception raised while sending slack notification. \n Exception message: {}".format(e)
+ "Exception raised while sending slack notification. \n Exception message: {}".format(
+ e
+ )
)
@@ -235,4 +232,5 @@ def decorator(func):
if not (settings.DEBUG or settings.TEST):
return func
return aws_mocker(func)
+
return decorator
diff --git a/apps/jobs/admin.py b/apps/jobs/admin.py
index 7bc5c7fcac..ef1d28c946 100644
--- a/apps/jobs/admin.py
+++ b/apps/jobs/admin.py
@@ -72,18 +72,25 @@ def submit_job_to_worker(self, request, queryset):
message = {
"challenge_pk": challenge_id,
"phase_pk": challenge_phase_id,
- "submission_pk": submission.id
+ "submission_pk": submission.id,
}
if submission.challenge_phase.challenge.is_docker_based:
try:
- response = requests.get(submission.input_file)
+ response = requests.get(submission.input_file.url)
except Exception as e:
- messages.error(request, "Failed to get input_file with exception: {0}".format(e))
+ messages.error(
+ request,
+ "Failed to get input_file with exception: {0}".format(
+ e
+ ),
+ )
return
if response and response.status_code == 200:
- message["submitted_image_uri"] = response.json()["submitted_image_uri"]
+ message["submitted_image_uri"] = response.json()[
+ "submitted_image_uri"
+ ]
publish_submission_message(message)
queryset.update(status=Submission.SUBMITTED)
diff --git a/apps/jobs/views.py b/apps/jobs/views.py
index d862796dfa..b6184023ee 100644
--- a/apps/jobs/views.py
+++ b/apps/jobs/views.py
@@ -30,7 +30,7 @@
from base.utils import (
paginated_queryset,
StandardResultSetPagination,
- get_sqs_queue_object,
+ get_or_create_sqs_queue_object,
get_boto3_client,
)
from challenges.models import (
@@ -279,17 +279,19 @@ def challenge_submission(request, challenge_id, challenge_phase_id):
)
message = {
"challenge_pk": challenge_id,
- "phase_pk": challenge_phase_id
+ "phase_pk": challenge_phase_id,
}
if challenge.is_docker_based:
try:
- file_content = json.loads(
- request.FILES['input_file'].read()
- )
- message["submitted_image_uri"] = file_content["submitted_image_uri"]
- except:
+ file_content = json.loads(request.FILES["input_file"].read())
+ message["submitted_image_uri"] = file_content[
+ "submitted_image_uri"
+ ]
+ except Exception as ex:
response_data = {
- "error": "Error in deserializing submitted_image_uri from submission file"
+ "error": "Error {} in submitted_image_uri from submission file".format(
+ ex
+ )
}
return Response(
response_data, status=status.HTTP_400_BAD_REQUEST
@@ -1047,7 +1049,7 @@ def re_run_submission(request, submission_pk):
message = {
"challenge_pk": challenge.pk,
"phase_pk": challenge_phase.pk,
- "submission_pk": submission.pk
+ "submission_pk": submission.pk,
}
if submission.challenge_phase.challenge.is_docker_based:
@@ -1055,12 +1057,18 @@ def re_run_submission(request, submission_pk):
response = requests.get(submission.input_file)
except Exception as e:
response_data = {
- "error": "Failed to get submission input file with error: {0}".format(e)
+ "error": "Failed to get submission input file with error: {0}".format(
+ e
+ )
}
- return Response(response_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ return Response(
+ response_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
if response and response.status_code == 200:
- message["submitted_image_uri"] = response.json()["submitted_image_uri"]
+ message["submitted_image_uri"] = response.json()[
+ "submitted_image_uri"
+ ]
publish_submission_message(message)
response_data = {
@@ -1137,7 +1145,7 @@ def get_submission_message_from_queue(request, queue_name):
}
return Response(response_data, status=status.HTTP_401_UNAUTHORIZED)
- queue = get_sqs_queue_object()
+ queue = get_or_create_sqs_queue_object(queue_name)
try:
messages = queue.receive_messages()
if len(messages):
@@ -1193,7 +1201,7 @@ def delete_submission_message_from_queue(request, queue_name):
}
return Response(response_data, status=status.HTTP_401_UNAUTHORIZED)
- queue = get_sqs_queue_object(queue_name)
+ queue = get_or_create_sqs_queue_object(queue_name)
try:
message = queue.Message(receipt_handle)
message.delete()
diff --git a/scripts/workers/worker_util.py b/scripts/workers/worker_util.py
index d31846e9a1..035f69aafb 100644
--- a/scripts/workers/worker_util.py
+++ b/scripts/workers/worker_util.py
@@ -6,7 +6,7 @@
URLS = {
"get_message_from_sqs_queue": "/api/jobs/challenge/queues/{}/",
- "delete_message_from_sqs_queue": "/api/jobs/queues/{}/receipt/{}/",
+ "delete_message_from_sqs_queue": "/api/jobs/queues/{}/",
"get_submission_by_pk": "/api/jobs/submission/{}",
"get_challenge_phases_by_challenge_pk": "/api/challenges/{}/phases/",
"get_challenge_by_queue_name": "/api/challenges/challenge/queues/{}/",
@@ -51,11 +51,10 @@ def get_message_from_sqs_queue(self):
return response
def delete_message_from_sqs_queue(self, receipt_handle):
- url = URLS.get("delete_message_from_sqs_queue").format(
- self.QUEUE_NAME, receipt_handle
- )
+ url = URLS.get("delete_message_from_sqs_queue").format(self.QUEUE_NAME)
url = self.return_url_per_environment(url)
- response = self.make_request(url, "GET") # noqa
+ data = {"receipt_handle": receipt_handle}
+ response = self.make_request(url, "POST", data) # noqa
return response.status_code
def get_submission_by_pk(self, submission_pk):
From bf36a9d666ec1aa22238dd8a0793bb284adc77a1 Mon Sep 17 00:00:00 2001
From: Sanjeev Singh
Date: Wed, 1 Jan 2020 23:22:58 +0530
Subject: [PATCH 24/34] Backend: Add show_leaderboard_by_latest_submission
field in challenge phase split serializer(#2568)
* Add `show_leaderboard_by_latest_submission` field in challenge phase split serializer
- Added field `show_leaderboard_by_latest_submission` in `ChallengePhaseSplitSerializer` in order to get this fields through `challenge_phase_split_list` from a particular object of phase splits.
* Updated tests
---
apps/challenges/serializers.py | 1 +
tests/unit/challenges/test_views.py | 8 ++++++++
2 files changed, 9 insertions(+)
diff --git a/apps/challenges/serializers.py b/apps/challenges/serializers.py
index c2a88bd438..ac14d8512b 100644
--- a/apps/challenges/serializers.py
+++ b/apps/challenges/serializers.py
@@ -113,6 +113,7 @@ class Meta:
"challenge_phase_name",
"dataset_split_name",
"visibility",
+ "show_leaderboard_by_latest_submission"
)
def get_dataset_split_name(self, obj):
diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py
index f87819718a..7055737788 100644
--- a/tests/unit/challenges/test_views.py
+++ b/tests/unit/challenges/test_views.py
@@ -2543,6 +2543,7 @@ def setUp(self):
visibility=ChallengePhaseSplit.PUBLIC,
leaderboard_decimal_precision=2,
is_leaderboard_order_descending=True,
+ show_leaderboard_by_latest_submission=False
)
self.challenge_phase_split_host = ChallengePhaseSplit.objects.create(
@@ -2550,6 +2551,7 @@ def setUp(self):
challenge_phase=self.challenge_phase,
leaderboard=self.leaderboard,
visibility=ChallengePhaseSplit.HOST,
+ show_leaderboard_by_latest_submission=False
)
def tearDown(self):
@@ -2573,6 +2575,8 @@ def test_get_challenge_phase_split(self):
"dataset_split": self.dataset_split.id,
"dataset_split_name": self.dataset_split.name,
"visibility": self.challenge_phase_split.visibility,
+ "show_leaderboard_by_latest_submission":
+ self.challenge_phase_split.show_leaderboard_by_latest_submission
}
]
self.client.force_authenticate(user=self.participant_user)
@@ -2608,6 +2612,8 @@ def test_get_challenge_phase_split_when_user_is_challenge_host(self):
"dataset_split": self.dataset_split.id,
"dataset_split_name": self.dataset_split.name,
"visibility": self.challenge_phase_split.visibility,
+ "show_leaderboard_by_latest_submission":
+ self.challenge_phase_split.show_leaderboard_by_latest_submission
},
{
"id": self.challenge_phase_split_host.id,
@@ -2616,6 +2622,8 @@ def test_get_challenge_phase_split_when_user_is_challenge_host(self):
"dataset_split": self.dataset_split_host.id,
"dataset_split_name": self.dataset_split_host.name,
"visibility": self.challenge_phase_split_host.visibility,
+ "show_leaderboard_by_latest_submission":
+ self.challenge_phase_split_host.show_leaderboard_by_latest_submission
},
]
self.client.force_authenticate(user=self.user)
From ddae77748de73d67dc0555a9f55e74abcee0cece Mon Sep 17 00:00:00 2001
From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com>
Date: Tue, 7 Jan 2020 20:28:10 +0530
Subject: [PATCH 25/34] Docs: Update django version in architecture.md(#2579)
---
docs/source/architecture.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/source/architecture.md b/docs/source/architecture.md
index d3b14df209..abfda434de 100644
--- a/docs/source/architecture.md
+++ b/docs/source/architecture.md
@@ -6,7 +6,7 @@ EvalAI helps researchers, students, and data scientists to create, collaborate,
#### Django
-Django is the heart of the application, which powers our backend. We use Django version 1.11.18.
+Django is the heart of the application, which powers our backend. We use Django version 1.11.23.
#### Django Rest Framework
From 7942f07af0d1b729c46e9d490aba3f945ee2e391 Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Wed, 15 Jan 2020 13:05:56 -0500
Subject: [PATCH 26/34] RL submission worker: Fix http request returning
response in worker_utils(#2575)
---
scripts/workers/worker_util.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/workers/worker_util.py b/scripts/workers/worker_util.py
index 035f69aafb..e9f1c7605a 100644
--- a/scripts/workers/worker_util.py
+++ b/scripts/workers/worker_util.py
@@ -55,7 +55,7 @@ def delete_message_from_sqs_queue(self, receipt_handle):
url = self.return_url_per_environment(url)
data = {"receipt_handle": receipt_handle}
response = self.make_request(url, "POST", data) # noqa
- return response.status_code
+ return response
def get_submission_by_pk(self, submission_pk):
url = URLS.get("get_submission_by_pk").format(submission_pk)
From 81869ab86eb97baa0afdbbfdb6e66620d59328f0 Mon Sep 17 00:00:00 2001
From: Takitsuse Nagisa <57856193+takitsuse@users.noreply.github.com>
Date: Sun, 19 Jan 2020 12:11:23 +0900
Subject: [PATCH 27/34] Frontend: Add feature to update
maximum_submissions_per_month using UI(#2540)
Co-authored-by: Rishabh Jain
---
frontend/src/js/controllers/challengeCtrl.js | 2 ++
frontend/tests/controllers-test/challengeCtrl.test.js | 1 +
2 files changed, 3 insertions(+)
diff --git a/frontend/src/js/controllers/challengeCtrl.js b/frontend/src/js/controllers/challengeCtrl.js
index 9bcb711561..3b19106b8d 100644
--- a/frontend/src/js/controllers/challengeCtrl.js
+++ b/frontend/src/js/controllers/challengeCtrl.js
@@ -1891,6 +1891,7 @@
vm.challengePhaseDialog = function(ev, phase) {
vm.page.challenge_phase = phase;
vm.page.max_submissions_per_day = phase.max_submissions_per_day;
+ vm.page.max_submissions_per_month = phase.max_submissions_per_month;
vm.phaseStartDate = moment(phase.start_date);
vm.phaseEndDate = moment(phase.end_date);
vm.testAnnotationFile = null;
@@ -1916,6 +1917,7 @@
formData.append("start_date", vm.phaseStartDate.toISOString());
formData.append("end_date", vm.phaseEndDate.toISOString());
formData.append("max_submissions_per_day", vm.page.challenge_phase.max_submissions_per_day);
+ formData.append("max_submissions_per_month", vm.page.challenge_phase.max_submissions_per_month);
formData.append("max_submissions", vm.page.challenge_phase.max_submissions);
if (vm.testAnnotationFile) {
formData.append("test_annotation", vm.testAnnotationFile);
diff --git a/frontend/tests/controllers-test/challengeCtrl.test.js b/frontend/tests/controllers-test/challengeCtrl.test.js
index 6682b80191..d516d19746 100644
--- a/frontend/tests/controllers-test/challengeCtrl.test.js
+++ b/frontend/tests/controllers-test/challengeCtrl.test.js
@@ -2280,6 +2280,7 @@ describe('Unit tests for challenge controller', function () {
vm.challengePhaseDialog(ev, phase);
expect(vm.page.challenge_phase).toEqual(phase);
expect(vm.page.max_submissions_per_day).toEqual(phase.max_submissions_per_day);
+ expect(vm.page.max_submissions_per_month).toEqual(phase.max_submissions_per_month);
expect(vm.phaseStartDate).toEqual(moment(phase.start_date));
expect(vm.phaseEndDate).toEqual(moment(phase.end_date));
expect(vm.testAnnotationFile).toEqual(null);
From 963a989dbef2b1ad0156dc33b1df4ecb4802ae63 Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Mon, 20 Jan 2020 00:05:15 -0500
Subject: [PATCH 28/34] Backend: Add Job ID in submission model for code upload
submission evaluation(#2631)
---
...13_add_job_id_field_in_submission_model.py | 25 +++++++++++++++++++
apps/jobs/models.py | 7 ++++++
2 files changed, 32 insertions(+)
create mode 100644 apps/jobs/migrations/0013_add_job_id_field_in_submission_model.py
diff --git a/apps/jobs/migrations/0013_add_job_id_field_in_submission_model.py b/apps/jobs/migrations/0013_add_job_id_field_in_submission_model.py
new file mode 100644
index 0000000000..eac39a7d20
--- /dev/null
+++ b/apps/jobs/migrations/0013_add_job_id_field_in_submission_model.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.23 on 2020-01-20 04:55
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [("jobs", "0012_add_baseline_submission")]
+
+ operations = [
+ migrations.AddField(
+ model_name="submission",
+ name="job_id",
+ field=django.contrib.postgres.fields.ArrayField(
+ base_field=models.TextField(blank=True, null=True),
+ blank=True,
+ default=[],
+ null=True,
+ size=None,
+ ),
+ )
+ ]
diff --git a/apps/jobs/models.py b/apps/jobs/models.py
index 7da628f6ad..eb5535c8ae 100644
--- a/apps/jobs/models.py
+++ b/apps/jobs/models.py
@@ -3,6 +3,7 @@
import logging
from django.contrib.auth.models import User
+from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models import Max
from rest_framework.exceptions import PermissionDenied
@@ -107,6 +108,12 @@ class Submission(TimeStampedModel):
publication_url = models.CharField(max_length=1000, default="", blank=True)
project_url = models.CharField(max_length=1000, default="", blank=True)
is_baseline = models.BooleanField(default=False)
+ job_id = ArrayField(
+ models.TextField(null=True, blank=True),
+ default=[],
+ blank=True,
+ null=True,
+ )
def __str__(self):
return "{}".format(self.id)
From 033299e11fbaebfc1a905b6699fc3768e8e74f14 Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Mon, 20 Jan 2020 00:30:30 -0500
Subject: [PATCH 29/34] Backend: Rename job_id to job_name in submission model
to be consistent with kubernetes(#2632)
---
...b_id_field_in_submission_model_to_job_name.py | 16 ++++++++++++++++
apps/jobs/models.py | 2 +-
2 files changed, 17 insertions(+), 1 deletion(-)
create mode 100644 apps/jobs/migrations/0014_rename_job_id_field_in_submission_model_to_job_name.py
diff --git a/apps/jobs/migrations/0014_rename_job_id_field_in_submission_model_to_job_name.py b/apps/jobs/migrations/0014_rename_job_id_field_in_submission_model_to_job_name.py
new file mode 100644
index 0000000000..159e4d49c2
--- /dev/null
+++ b/apps/jobs/migrations/0014_rename_job_id_field_in_submission_model_to_job_name.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.23 on 2020-01-20 05:23
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [("jobs", "0013_add_job_id_field_in_submission_model")]
+
+ operations = [
+ migrations.RenameField(
+ model_name="submission", old_name="job_id", new_name="job_name"
+ )
+ ]
diff --git a/apps/jobs/models.py b/apps/jobs/models.py
index eb5535c8ae..ba53431466 100644
--- a/apps/jobs/models.py
+++ b/apps/jobs/models.py
@@ -108,7 +108,7 @@ class Submission(TimeStampedModel):
publication_url = models.CharField(max_length=1000, default="", blank=True)
project_url = models.CharField(max_length=1000, default="", blank=True)
is_baseline = models.BooleanField(default=False)
- job_id = ArrayField(
+ job_name = ArrayField(
models.TextField(null=True, blank=True),
default=[],
blank=True,
From 334c7d274b5c7c2c0ad6a089941f23424a3c411b Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Wed, 22 Jan 2020 01:47:50 -0500
Subject: [PATCH 30/34] Backend: Add feature to update job_name in
update_submission API(#2637)
* Add feature to update job name in submission
* Fix test cases
---
apps/jobs/admin.py | 1 +
apps/jobs/serializers.py | 1 +
apps/jobs/views.py | 11 +++++++-
tests/unit/challenges/test_views.py | 24 +++++++----------
tests/unit/jobs/test_views.py | 41 +++++++++++++++++++----------
5 files changed, 48 insertions(+), 30 deletions(-)
diff --git a/apps/jobs/admin.py b/apps/jobs/admin.py
index ef1d28c946..34e250779c 100644
--- a/apps/jobs/admin.py
+++ b/apps/jobs/admin.py
@@ -34,6 +34,7 @@ class SubmissionAdmin(ImportExportTimeStampedAdmin):
"stderr_file",
"submission_result_file",
"submission_metadata_file",
+ "job_name",
)
list_filter = (
"challenge_phase__challenge",
diff --git a/apps/jobs/serializers.py b/apps/jobs/serializers.py
index ab848a3950..c6deaea9eb 100644
--- a/apps/jobs/serializers.py
+++ b/apps/jobs/serializers.py
@@ -50,6 +50,7 @@ class Meta:
"submission_result_file",
"when_made_public",
"is_baseline",
+ "job_name",
)
def get_participant_team_name(self, obj):
diff --git a/apps/jobs/views.py b/apps/jobs/views.py
index b6184023ee..db88387aaf 100644
--- a/apps/jobs/views.py
+++ b/apps/jobs/views.py
@@ -999,11 +999,20 @@ def update_submission(request, challenge_pk):
if request.method == "PATCH":
submission_pk = request.data.get("submission")
submission_status = request.data.get("submission_status", "").lower()
+ job_name = request.data.get("job_name", "").lower()
submission = get_submission_model(submission_pk)
+ jobs = submission.job_name
+ if job_name:
+ jobs.append(job_name)
if submission_status not in [Submission.RUNNING]:
response_data = {"error": "Sorry, submission status is invalid"}
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
- data = {"status": submission_status, "started_at": timezone.now()}
+
+ data = {
+ "status": submission_status,
+ "started_at": timezone.now(),
+ "job_name": jobs,
+ }
serializer = SubmissionSerializer(
submission, data=data, partial=True, context={"request": request}
)
diff --git a/tests/unit/challenges/test_views.py b/tests/unit/challenges/test_views.py
index 7055737788..9eaf29801f 100644
--- a/tests/unit/challenges/test_views.py
+++ b/tests/unit/challenges/test_views.py
@@ -2543,7 +2543,7 @@ def setUp(self):
visibility=ChallengePhaseSplit.PUBLIC,
leaderboard_decimal_precision=2,
is_leaderboard_order_descending=True,
- show_leaderboard_by_latest_submission=False
+ show_leaderboard_by_latest_submission=False,
)
self.challenge_phase_split_host = ChallengePhaseSplit.objects.create(
@@ -2551,7 +2551,7 @@ def setUp(self):
challenge_phase=self.challenge_phase,
leaderboard=self.leaderboard,
visibility=ChallengePhaseSplit.HOST,
- show_leaderboard_by_latest_submission=False
+ show_leaderboard_by_latest_submission=False,
)
def tearDown(self):
@@ -2575,8 +2575,7 @@ def test_get_challenge_phase_split(self):
"dataset_split": self.dataset_split.id,
"dataset_split_name": self.dataset_split.name,
"visibility": self.challenge_phase_split.visibility,
- "show_leaderboard_by_latest_submission":
- self.challenge_phase_split.show_leaderboard_by_latest_submission
+ "show_leaderboard_by_latest_submission": self.challenge_phase_split.show_leaderboard_by_latest_submission,
}
]
self.client.force_authenticate(user=self.participant_user)
@@ -2612,8 +2611,7 @@ def test_get_challenge_phase_split_when_user_is_challenge_host(self):
"dataset_split": self.dataset_split.id,
"dataset_split_name": self.dataset_split.name,
"visibility": self.challenge_phase_split.visibility,
- "show_leaderboard_by_latest_submission":
- self.challenge_phase_split.show_leaderboard_by_latest_submission
+ "show_leaderboard_by_latest_submission": self.challenge_phase_split.show_leaderboard_by_latest_submission,
},
{
"id": self.challenge_phase_split_host.id,
@@ -2622,8 +2620,7 @@ def test_get_challenge_phase_split_when_user_is_challenge_host(self):
"dataset_split": self.dataset_split_host.id,
"dataset_split_name": self.dataset_split_host.name,
"visibility": self.challenge_phase_split_host.visibility,
- "show_leaderboard_by_latest_submission":
- self.challenge_phase_split_host.show_leaderboard_by_latest_submission
+ "show_leaderboard_by_latest_submission": self.challenge_phase_split_host.show_leaderboard_by_latest_submission,
},
]
self.client.force_authenticate(user=self.user)
@@ -3206,6 +3203,7 @@ def test_get_all_submissions_when_user_is_participant_of_challenge(self):
"is_flagged": self.submission1.is_flagged,
"when_made_public": self.submission1.when_made_public,
"is_baseline": self.submission1.is_baseline,
+ "job_name": self.submission1.job_name,
}
]
self.challenge5.participant_teams.add(self.participant_team6)
@@ -3720,7 +3718,7 @@ def test_get_dataset_split(self):
"visibility": self.challenge_phase_split.visibility,
"leaderboard_decimal_precision": self.challenge_phase_split.leaderboard_decimal_precision,
"is_leaderboard_order_descending": self.challenge_phase_split.is_leaderboard_order_descending,
- "show_leaderboard_by_latest_submission": self.challenge_phase_split.show_leaderboard_by_latest_submission
+ "show_leaderboard_by_latest_submission": self.challenge_phase_split.show_leaderboard_by_latest_submission,
}
response = self.client.get(self.url)
self.assertEqual(response.data, expected)
@@ -4078,9 +4076,7 @@ def test_get_aws_credentials_when_challenge_is_not_docker_based(self):
"challenges:get_aws_credentials_for_participant_team",
kwargs={"phase_pk": self.challenge_phase.pk},
)
- expected = {
- "error": "Sorry, this is not a docker based challenge."
- }
+ expected = {"error": "Sorry, this is not a docker based challenge."}
response = self.client.get(self.url, {})
self.assertEqual(response.data, expected)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@@ -4090,9 +4086,7 @@ def test_get_aws_credentials_when_not_participated(self):
"challenges:get_aws_credentials_for_participant_team",
kwargs={"phase_pk": self.challenge_phase.pk},
)
- expected = {
- "error": "You have not participated in this challenge."
- }
+ expected = {"error": "You have not participated in this challenge."}
self.client.force_authenticate(user=self.user2)
response = self.client.get(self.url, {})
self.assertEqual(response.data, expected)
diff --git a/tests/unit/jobs/test_views.py b/tests/unit/jobs/test_views.py
index 9645c0bd46..878614e2b9 100644
--- a/tests/unit/jobs/test_views.py
+++ b/tests/unit/jobs/test_views.py
@@ -172,7 +172,7 @@ def setUp(self):
)
self.rl_submission_file = SimpleUploadedFile(
- "submission.json", b'{"submitted_image_uri": "evalai-repo.com"}',
+ "submission.json", b'{"submitted_image_uri": "evalai-repo.com"}'
)
def tearDown(self):
@@ -638,6 +638,7 @@ def test_get_challenge_submissions(self):
"is_flagged": self.submission.is_flagged,
"when_made_public": self.submission.when_made_public,
"is_baseline": self.submission.is_baseline,
+ "job_name": self.submission.job_name,
}
]
self.challenge.participant_teams.add(self.participant_team)
@@ -1309,6 +1310,7 @@ def test_change_submission_data_and_visibility_when_submission_exist(self):
self.submission.when_made_public.isoformat(), "Z"
).replace("+00:00", ""),
"is_baseline": self.submission.is_baseline,
+ "job_name": self.submission.job_name,
}
self.challenge.participant_teams.add(self.participant_team)
response = self.client.patch(self.url, self.data)
@@ -1354,6 +1356,7 @@ def test_change_submission_data_and_visibility_when_challenge_phase_is_private_a
self.private_submission.when_made_public.isoformat(), "Z"
).replace("+00:00", ""),
"is_baseline": self.submission.is_baseline,
+ "job_name": self.submission.job_name,
}
self.client.force_authenticate(user=self.user)
@@ -1417,15 +1420,14 @@ def test_change_submission_data_and_visibility_when_is_public_is_false(
self.submission.when_made_public.isoformat(), "Z"
).replace("+00:00", ""),
"is_baseline": self.submission.is_baseline,
+ "job_name": self.submission.job_name,
}
self.challenge.participant_teams.add(self.participant_team)
response = self.client.patch(self.url, self.data)
self.assertEqual(response.data, expected)
self.assertEqual(response.status_code, status.HTTP_200_OK)
- def test_toggle_baseline_when_user_is_not_a_host(
- self
- ):
+ def test_toggle_baseline_when_user_is_not_a_host(self):
self.url = reverse_lazy(
"jobs:change_submission_data_and_visibility",
kwargs={
@@ -1437,14 +1439,14 @@ def test_toggle_baseline_when_user_is_not_a_host(
self.data = {"is_baseline": True}
self.challenge.save()
self.client.force_authenticate(user=self.user1)
- expected = {"error": "Sorry, you are not authorized to make this request"}
+ expected = {
+ "error": "Sorry, you are not authorized to make this request"
+ }
response = self.client.patch(self.url, self.data)
self.assertEqual(response.data, expected)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- def test_toggle_baseline_when_user_is_host(
- self
- ):
+ def test_toggle_baseline_when_user_is_host(self):
self.url = reverse_lazy(
"jobs:change_submission_data_and_visibility",
kwargs={
@@ -1474,14 +1476,17 @@ def test_toggle_baseline_when_user_is_host(
"stderr_file": None,
"submission_result_file": None,
"submitted_at": "{0}{1}".format(
- self.host_participant_team_submission.submitted_at.isoformat(), "Z"
+ self.host_participant_team_submission.submitted_at.isoformat(),
+ "Z",
).replace("+00:00", ""),
"is_public": self.host_participant_team_submission.is_public,
"is_flagged": self.host_participant_team_submission.is_flagged,
"when_made_public": "{0}{1}".format(
- self.host_participant_team_submission.when_made_public.isoformat(), "Z"
+ self.host_participant_team_submission.when_made_public.isoformat(),
+ "Z",
).replace("+00:00", ""),
"is_baseline": True,
+ "job_name": self.host_participant_team_submission.job_name,
}
response = self.client.patch(self.url, self.data)
self.assertEqual(response.data, expected)
@@ -1555,6 +1560,7 @@ def test_get_submission_by_pk_when_user_created_the_submission(self):
self.submission.when_made_public.isoformat(), "Z"
).replace("+00:00", ""),
"is_baseline": self.submission.is_baseline,
+ "job_name": self.submission.job_name,
}
self.client.force_authenticate(user=self.submission.created_by)
@@ -1595,6 +1601,7 @@ def test_get_submission_by_pk_when_user_is_challenge_host(self):
self.submission.when_made_public.isoformat(), "Z"
).replace("+00:00", ""),
"is_baseline": self.submission.is_baseline,
+ "job_name": self.submission.job_name,
}
self.client.force_authenticate(user=self.user)
@@ -1734,9 +1741,15 @@ def setUp(self):
self.result_json_2 = {"score": 10.0, "test-score": 20.0}
- self.result_json_host_participant_team = {"score": 52.0, "test-score": 80.0}
+ self.result_json_host_participant_team = {
+ "score": 52.0,
+ "test-score": 80.0,
+ }
- self.result_json_host_participant_team_2 = {"score": 20.0, "test-score": 60.0}
+ self.result_json_host_participant_team_2 = {
+ "score": 20.0,
+ "test-score": 60.0,
+ }
self.expected_results = [
self.result_json["score"],
@@ -1889,7 +1902,7 @@ def test_get_leaderboard_with_baseline_entry(self):
"submission__submitted_at": self.submission.submitted_at,
"submission__is_baseline": False,
"submission__method_name": self.submission.method_name,
- }
+ },
],
}
expected = collections.OrderedDict(expected)
@@ -1975,7 +1988,7 @@ def test_get_leaderboard_with_multiple_baseline_entries(self):
"submission__submitted_at": self.host_participant_team_submission_2.submitted_at,
"submission__is_baseline": True,
"submission__method_name": self.host_participant_team_submission_2.method_name,
- }
+ },
],
}
expected = collections.OrderedDict(expected)
From 8ca5848006c6b23d9a5482d94aa1a74e438df403 Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Wed, 22 Jan 2020 01:55:38 -0500
Subject: [PATCH 31/34] Code upload worker: Update script with deployment type
as Job & add feature to terminate job(#2638)
---
scripts/workers/rl_submission_worker.py | 148 ++++++++++++------
.../{worker_util.py => worker_utils.py} | 0
2 files changed, 104 insertions(+), 44 deletions(-)
rename scripts/workers/{worker_util.py => worker_utils.py} (100%)
diff --git a/scripts/workers/rl_submission_worker.py b/scripts/workers/rl_submission_worker.py
index 6089c1ab8b..ca62357fca 100644
--- a/scripts/workers/rl_submission_worker.py
+++ b/scripts/workers/rl_submission_worker.py
@@ -1,12 +1,14 @@
import logging
import os
import signal
-import time
-from worker_util import EvalAI_Interface
+from worker_utils import EvalAI_Interface
from kubernetes import client, config
+# TODO: Add exception in all the commands
+# from kubernetes.client.rest import ApiException
+
class GracefulKiller:
kill_now = False
@@ -20,6 +22,8 @@ def exit_gracefully(self, signum, frame):
logger = logging.getLogger(__name__)
+config.load_kube_config()
+batch_v1 = client.BatchV1Api()
AUTH_TOKEN = os.environ.get("AUTH_TOKEN", "auth_token")
EVALAI_API_SERVER = os.environ.get(
@@ -27,19 +31,31 @@ def exit_gracefully(self, signum, frame):
)
QUEUE_NAME = os.environ.get("QUEUE_NAME", "evalai_submission_queue")
ENVIRONMENT_IMAGE = os.environ.get("ENVIRONMENT_IMAGE", "image_name:tag")
-MESSAGE_FETCH_DEPLAY = int(os.environ.get("MESSAGE_FETCH_DEPLAY", "5"))
-def create_deployment_object(image, submission, message):
+def create_job_object(message):
+ """Function to create the AWS EKS Job object
+
+ Arguments:
+ message {[dict]} -- Submission message from AWS SQS queue
+
+ Returns:
+ [AWS EKS Job class object] -- AWS EKS Job class object
+ """
+
PYTHONUNBUFFERED_ENV = client.V1EnvVar(name="PYTHONUNBUFFERED", value="1")
AUTH_TOKEN_ENV = client.V1EnvVar(name="AUTH_TOKEN", value=AUTH_TOKEN)
EVALAI_API_SERVER_ENV = client.V1EnvVar(
name="EVALAI_API_SERVER", value=EVALAI_API_SERVER
)
MESSAGE_BODY_ENV = client.V1EnvVar(name="BODY", value=str(message))
+ submission_pk = message["submission_pk"]
+ image = message["submitted_image_uri"]
+ # Configureate Pod agent container
agent_container = client.V1Container(
name="agent", image=image, env=[PYTHONUNBUFFERED_ENV]
)
+ # Configureate Pod environment container
environment_container = client.V1Container(
name="environment",
image=ENVIRONMENT_IMAGE,
@@ -50,85 +66,129 @@ def create_deployment_object(image, submission, message):
MESSAGE_BODY_ENV,
],
)
+ # Create and configurate a spec section
template = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(labels={"app": "evaluation"}),
spec=client.V1PodSpec(
- containers=[environment_container, agent_container]
+ containers=[environment_container, agent_container],
+ restart_policy="Never",
),
)
- spec = client.ExtensionsV1beta1DeploymentSpec(
- replicas=1, template=template
- )
- deployment = client.ExtensionsV1beta1Deployment(
- api_version="extensions/v1beta1",
- kind="Deployment",
- metadata=client.V1ObjectMeta(name="submission-{0}".format(submission)),
+ # Create the specification of deployment
+ spec = client.V1JobSpec(backoff_limit=1, template=template)
+ # Instantiate the job object
+ job = client.V1Job(
+ api_version="batch/v1",
+ kind="Job",
+ metadata=client.V1ObjectMeta(
+ name="submission-{0}".format(submission_pk)
+ ),
spec=spec,
)
- return deployment
+ return job
+
+
+def create_job(api_instance, job):
+ """Function to create a job on AWS EKS cluster
+ Arguments:
+ api_instance {[AWS EKS API object]} -- API object for creating job
+ job {[AWS EKS job object]} -- Job object returned after running create_job_object fucntion
-def create_deployment(api_instance, deployment):
- api_response = api_instance.create_namespaced_deployment(
- body=deployment, namespace="default"
+ Returns:
+ [V1Job object] -- [AWS EKS V1Job]
+ For reference: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Job.md
+ """
+ api_response = api_instance.create_namespaced_job(
+ body=job, namespace="default", pretty=True
)
logger.info("Deployment created. status='%s'" % str(api_response.status))
+ return api_response
-def process_submission_callback(message, api):
- config.load_kube_config()
- extensions_v1beta1 = client.ExtensionsV1beta1Api()
- logger.info(message)
- submission_data = {
- "submission_status": "running",
- "submission": message["submission_pk"],
- }
- logger.info(submission_data)
- api.update_submission_status(submission_data, message["challenge_pk"])
- dep = create_deployment_object(
- message["submitted_image_uri"], message["submission_pk"], message
+def delete_job(api_instance, job_name):
+ """Function to delete a job on AWS EKS cluster
+
+ Arguments:
+ api_instance {[AWS EKS API object]} -- API object for deleting job
+ job_name {[string]} -- Name of the job to be terminated
+ """
+ api_response = api_instance.delete_namespaced_job(
+ name=job_name,
+ namespace="default",
+ body=client.V1DeleteOptions(
+ propagation_policy="Foreground", grace_period_seconds=5
+ ),
)
- create_deployment(extensions_v1beta1, dep)
+ logger.info("Job deleted. status='%s'" % str(api_response.status))
+
+
+def process_submission_callback(body, evalai):
+ """Function to process submission message from SQS Queue
+
+ Arguments:
+ body {[dict]} -- Submission message body from AWS SQS Queue
+ evalai {[EvalAI class object]} -- EvalAI class object imported from worker_utils
+ """
+ try:
+ logger.info("[x] Received submission message %s" % body)
+ job = create_job_object(body)
+ response = create_job(batch_v1, job)
+ submission_data = {
+ "submission_status": "running",
+ "submission": body["submission_pk"],
+ "job_name": response.metadata.name,
+ }
+ evalai.update_submission_status(submission_data, body["challenge_pk"])
+ except Exception as e:
+ logger.exception(
+ "Exception while receiving message from submission queue with error {}".format(
+ e
+ )
+ )
def main():
- api = EvalAI_Interface(
+ killer = GracefulKiller()
+ evalai = EvalAI_Interface(
AUTH_TOKEN=AUTH_TOKEN,
EVALAI_API_SERVER=EVALAI_API_SERVER,
QUEUE_NAME=QUEUE_NAME,
)
logger.info(
- "String RL Worker for {}".format(
- api.get_challenge_by_queue_name()["title"]
+ "Deploying Worker for {}".format(
+ evalai.get_challenge_by_queue_name()["title"]
)
)
- killer = GracefulKiller()
while True:
logger.info(
"Fetching new messages from the queue {}".format(QUEUE_NAME)
)
- message = api.get_message_from_sqs_queue()
- logger.info(message)
+ message = evalai.get_message_from_sqs_queue()
message_body = message.get("body")
if message_body:
submission_pk = message_body.get("submission_pk")
- submission = api.get_submission_by_pk(submission_pk)
+ submission = evalai.get_submission_by_pk(submission_pk)
if submission:
- if submission.get("status") == "finished":
+ if (
+ submission.get("status") == "finished"
+ or submission.get("status") == "failed"
+ ):
+ # Fetch the last job name from the list as it is the latest running job
+ job_name = submission.get("job_name")[-1]
+ delete_job(batch_v1, job_name)
message_receipt_handle = message.get("receipt_handle")
- api.delete_message_from_sqs_queue(message_receipt_handle)
+ evalai.delete_message_from_sqs_queue(
+ message_receipt_handle
+ )
elif submission.get("status") == "running":
continue
else:
message_receipt_handle = message.get("receipt_handle")
logger.info(
- "Processing message body: {}".format(message_body)
- )
- process_submission_callback(message_body, api)
- api.delete_message_from_sqs_queue(
- message.get("receipt_handle")
+ "Processing message body: {0}".format(message_body)
)
- time.sleep(MESSAGE_FETCH_DEPLAY)
+ process_submission_callback(message_body, evalai)
if killer.kill_now:
break
diff --git a/scripts/workers/worker_util.py b/scripts/workers/worker_utils.py
similarity index 100%
rename from scripts/workers/worker_util.py
rename to scripts/workers/worker_utils.py
From 2fb0656e6f4fac1a76eed3b0d8bb0a859863c0aa Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Mon, 3 Feb 2020 00:39:24 -0500
Subject: [PATCH 32/34] Frontend: Add stats on the landing page(#2649)
---
frontend/src/views/web/landing.html | 80 +++++++++++++++++++++++++++--
1 file changed, 75 insertions(+), 5 deletions(-)
diff --git a/frontend/src/views/web/landing.html b/frontend/src/views/web/landing.html
index f5d523216b..ac9a1ab604 100644
--- a/frontend/src/views/web/landing.html
+++ b/frontend/src/views/web/landing.html
@@ -8,7 +8,7 @@
Evaluating state-of-the-art in AI
EvalAI is an open source platform for evaluating and
- comparing machine learning (ML) and artificial intelligence algorithms (AI) at scale.
+ comparing machine learning (ML) and artificial intelligence (AI) algorithms at scale.
+
+
+
+
+
+
+
+
+ AI Hosted Challenges
+
+
+
+
+
+
+
+
+
+
+
+
Features
@@ -97,7 +167,7 @@ Faster evaluation
-
+
Popular challenges
@@ -139,7 +209,7 @@ Popular challenges
-
+
@@ -255,7 +325,7 @@
Partner Organizations
-
+
@@ -264,7 +334,7 @@
Partner Organizations
-
+
Cite our work
From 1b2da22d4c90900be94b7fee145ff23940593b43 Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Mon, 3 Feb 2020 14:18:59 -0500
Subject: [PATCH 33/34] Backend: Add filtering by past, present & future
challenges in challenge admin(#2650)
---
apps/challenges/admin.py | 4 ++++
apps/challenges/admin_filters.py | 38 ++++++++++++++++++++++++++++++++
2 files changed, 42 insertions(+)
create mode 100644 apps/challenges/admin_filters.py
diff --git a/apps/challenges/admin.py b/apps/challenges/admin.py
index 44eca234eb..c79ea49a08 100644
--- a/apps/challenges/admin.py
+++ b/apps/challenges/admin.py
@@ -1,5 +1,6 @@
from django import forms
from django.contrib import admin, messages
+
from django.contrib.admin.helpers import ActionForm
from base.admin import ImportExportTimeStampedAdmin
@@ -12,6 +13,8 @@
stop_workers,
)
+from .admin_filters import ChallengeFilter
+
from .models import (
Challenge,
ChallengeConfiguration,
@@ -52,6 +55,7 @@ class ChallengeAdmin(ImportExportTimeStampedAdmin):
"task_def_arn",
)
list_filter = (
+ ChallengeFilter,
"published",
"is_registration_open",
"enable_forum",
diff --git a/apps/challenges/admin_filters.py b/apps/challenges/admin_filters.py
new file mode 100644
index 0000000000..120ba37dd6
--- /dev/null
+++ b/apps/challenges/admin_filters.py
@@ -0,0 +1,38 @@
+from django.contrib.admin import SimpleListFilter
+from django.utils import timezone
+
+
+class ChallengeFilter(SimpleListFilter):
+
+ title = "Challenges"
+ parameter_name = "challenge"
+
+ def lookups(self, request, model_admin):
+ options = [
+ ("past", "Past"),
+ ("present", "Ongoing"),
+ ("future", "Upcoming"),
+ ]
+ return options
+
+ def queryset(self, request, queryset):
+ q_params = {
+ "published": True,
+ "approved_by_admin": True,
+ "is_disabled": False,
+ }
+ if self.value() == "past":
+ q_params["end_date__lt"] = timezone.now()
+ challenges = queryset.filter(**q_params)
+ return challenges
+
+ elif self.value() == "present":
+ q_params["start_date__lt"] = timezone.now()
+ q_params["end_date__gt"] = timezone.now()
+ challenges = queryset.filter(**q_params)
+ return challenges
+
+ elif self.value() == "future":
+ q_params["start_date__gt"] = timezone.now()
+ challenges = queryset.filter(**q_params)
+ return challenges
From c84316acc6ab1dad001c52037feb2f10c7c050da Mon Sep 17 00:00:00 2001
From: Rishabh Jain
Date: Mon, 3 Feb 2020 17:01:20 -0500
Subject: [PATCH 34/34] Frontend: Aesthetic changes for stats on landing page
(#2652)
---
frontend/src/views/web/landing.html | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/frontend/src/views/web/landing.html b/frontend/src/views/web/landing.html
index ac9a1ab604..e9c4786675 100644
--- a/frontend/src/views/web/landing.html
+++ b/frontend/src/views/web/landing.html
@@ -32,13 +32,13 @@ Evaluating state-of-the-art in AI
- AI Hosted Challenges
+ Hosted AI Challenges
@@ -48,7 +48,7 @@
45+