Repository: oppia/oppia

Issue:
Replace requests library in the backend
Remove requests library as it is used in only one spot (while using mailgun API). As we are using it in a very specific spot, we should try to find a way which doesn't require a whole 3rd party library. 

As for context, this issue was brought to light after the release-2.8.3 broke after the requests library was upgraded. See https://github.com/oppia/oppia/pull/7088 for more details.
Replace requests library in the backend
Remove requests library as it is used in only one spot (while using mailgun API). As we are using it in a very specific spot, we should try to find a way which doesn't require a whole 3rd party library. 

As for context, this issue was brought to light after the release-2.8.3 broke after the requests library was upgraded. See https://github.com/oppia/oppia/pull/7088 for more details.


Discussion:
Hey @nithusha21  Can I work on this?
Hey @nithusha21  Can I work on this?

Resolving patch:
diff --git a/appengine_config.py b/appengine_config.py
--- a/appengine_config.py
+++ b/appengine_config.py
@@ -97,7 +97,6 @@ def save(self):
     os.path.join(ROOT_PATH, 'third_party', 'graphy-1.0.0'),
     os.path.join(ROOT_PATH, 'third_party', 'html5lib-python-1.0.1'),
     os.path.join(ROOT_PATH, 'third_party', 'mutagen-1.42.0'),
-    os.path.join(ROOT_PATH, 'third_party', 'requests-2.10.0'),
     os.path.join(ROOT_PATH, 'third_party', 'simplejson-3.16.0'),
     os.path.join(ROOT_PATH, 'third_party', 'six-1.12.0'),
     os.path.join(ROOT_PATH, 'third_party', 'soupsieve-1.9.1'),
diff --git a/core/platform/email/mailgun_email_services.py b/core/platform/email/mailgun_email_services.py
--- a/core/platform/email/mailgun_email_services.py
+++ b/core/platform/email/mailgun_email_services.py
@@ -16,10 +16,39 @@
 
 """Provides mailgun api to send email."""
 
+import base64
+import urllib
+import urllib2
+
 from core.platform.email import gae_email_services
 import feconf
 
-import requests
+
+def post_to_mailgun(data):
+    """Send POST HTTP request to mailgun api. This method is adopted from
+    the requests library's post method.
+
+    Args:
+        - data: dict. The data to be sent in the request's body.
+
+    Returns:
+         Response from the server. The object is a file-like object.
+         https://docs.python.org/2/library/urllib2.html
+    """
+    if not feconf.MAILGUN_API_KEY:
+        raise Exception('Mailgun API key is not available.')
+
+    if not feconf.MAILGUN_DOMAIN_NAME:
+        raise Exception('Mailgun domain name is not set.')
+
+    encoded = base64.b64encode(b'api:%s' % feconf.MAILGUN_API_KEY).strip()
+    auth_str = 'Basic %s' % encoded
+    header = {'Authorization': auth_str}
+    server = (
+        'https://api.mailgun.net/v3/%s/messages' % feconf.MAILGUN_DOMAIN_NAME)
+    data = urllib.urlencode(data)
+    req = urllib2.Request(server, data, header)
+    return urllib2.urlopen(req)
 
 
 def send_mail(
@@ -49,15 +78,6 @@ def send_mail(
         feconf.MAILGUN_DOMAIN_NAME.
       (and possibly other exceptions, due to mail.send_mail() failures)
     """
-    if not feconf.MAILGUN_API_KEY:
-        raise Exception('Mailgun API key is not available.')
-
-    if not feconf.MAILGUN_DOMAIN_NAME:
-        raise Exception('Mailgun domain name is not set.')
-
-    mailgun_domain_name = (
-        'https://api.mailgun.net/v3/%s/messages' % feconf.MAILGUN_DOMAIN_NAME)
-
     if not feconf.CAN_SEND_EMAILS:
         raise Exception('This app cannot send emails to users.')
 
@@ -75,9 +95,7 @@ def send_mail(
         reply_to = gae_email_services.get_incoming_email_address(reply_to_id)
         data['h:Reply-To'] = reply_to
 
-    requests.post(
-        mailgun_domain_name, auth=('api', feconf.MAILGUN_API_KEY),
-        data=data)
+    post_to_mailgun(data)
 
 
 def send_bulk_mail(
@@ -106,15 +124,6 @@ def send_bulk_mail(
         feconf.MAILGUN_DOMAIN_NAME.
       (and possibly other exceptions, due to mail.send_mail() failures)
     """
-    if not feconf.MAILGUN_API_KEY:
-        raise Exception('Mailgun API key is not available.')
-
-    if not feconf.MAILGUN_DOMAIN_NAME:
-        raise Exception('Mailgun domain name is not set.')
-
-    mailgun_domain_name = (
-        'https://api.mailgun.net/v3/%s/messages' % feconf.MAILGUN_DOMAIN_NAME)
-
     if not feconf.CAN_SEND_EMAILS:
         raise Exception('This app cannot send emails to users.')
 
@@ -138,6 +147,4 @@ def send_bulk_mail(
             'html': html_body,
             'recipient-variables': '{}'}
 
-        requests.post(
-            mailgun_domain_name, auth=('api', feconf.MAILGUN_API_KEY),
-            data=data)
+        post_to_mailgun(data)


Test patch:
diff --git a/core/platform/email/mailgun_email_services_test.py b/core/platform/email/mailgun_email_services_test.py
--- a/core/platform/email/mailgun_email_services_test.py
+++ b/core/platform/email/mailgun_email_services_test.py
@@ -16,21 +16,40 @@
 
 """Tests for the Mailgun API wrapper."""
 
+import urllib2
+
 from core.platform.email import mailgun_email_services
 from core.tests import test_utils
 import feconf
-import requests
 
 
 class EmailTests(test_utils.GenericTestBase):
     """Tests for sending emails."""
 
+    def test_post_to_mailgun(self):
+        """Test for sending HTTP POST request."""
+        swapped_urlopen = lambda x: x
+        swapped_request = lambda *args: args
+        swap_urlopen_context = self.swap(urllib2, 'urlopen', swapped_urlopen)
+        swap_request_context = self.swap(
+            urllib2, 'Request', swapped_request)
+        swap_api = self.swap(feconf, 'MAILGUN_API_KEY', 'key')
+        swap_domain = self.swap(feconf, 'MAILGUN_DOMAIN_NAME', 'domain')
+        with swap_urlopen_context, swap_request_context, swap_api, swap_domain:
+            result = mailgun_email_services.post_to_mailgun({'data': 'data'})
+            expected = (
+                'https://api.mailgun.net/v3/domain/messages', 'data=data',
+                {'Authorization': 'Basic YXBpOmtleQ=='})
+            self.assertEqual(result, expected)
+
     def test_send_mail_raises_exception_for_missing_api_key(self):
         """Tests the missing Mailgun API key exception."""
         mailgun_api_exception = (
             self.assertRaisesRegexp(
                 Exception, 'Mailgun API key is not available.'))
-        with mailgun_api_exception:
+
+        allow_emailing = self.swap(feconf, 'CAN_SEND_EMAILS', True)
+        with mailgun_api_exception, allow_emailing:
             mailgun_email_services.send_mail(
                 feconf.SYSTEM_EMAIL_ADDRESS, feconf.ADMIN_EMAIL_ADDRESS,
                 'subject', 'body', 'html', bcc_admin=False)
@@ -41,7 +60,8 @@ def test_send_mail_raises_exception_for_missing_domain_name(self):
         mailgun_domain_name_exception = (
             self.assertRaisesRegexp(
                 Exception, 'Mailgun domain name is not set.'))
-        with mailgun_api, mailgun_domain_name_exception:
+        allow_emailing = self.swap(feconf, 'CAN_SEND_EMAILS', True)
+        with mailgun_api, mailgun_domain_name_exception, allow_emailing:
             mailgun_email_services.send_mail(
                 feconf.SYSTEM_EMAIL_ADDRESS, feconf.ADMIN_EMAIL_ADDRESS,
                 'subject', 'body', 'html', bcc_admin=False)
@@ -64,17 +84,19 @@ def test_send_mail_data_properly_sent(self):
         mailgun_domain = self.swap(feconf, 'MAILGUN_DOMAIN_NAME', 'domain')
         allow_emailing = self.swap(feconf, 'CAN_SEND_EMAILS', True)
 
-        # Data we expect to have been sent in the requests.post().
+        # Data we expect to have been sent in the
+        # mailgun_email_services.post_to_mailgun().
         expected = {'from': feconf.SYSTEM_EMAIL_ADDRESS,
                     'to': feconf.ADMIN_EMAIL_ADDRESS,
                     'subject': 'subject',
                     'text': 'body',
                     'html': 'html'}
 
-        # Lambda function, will replace requests.post() in send_mail.
-        req_post_lambda = (lambda domain_name, auth=None, data=None:
+        # Lambda function, will replace post_to_mailgun().
+        req_post_lambda = (lambda data=None:
                            self.assertDictContainsSubset(expected, data))
-        post_request = self.swap(requests, 'post', req_post_lambda)
+        post_request = self.swap(
+            mailgun_email_services, 'post_to_mailgun', req_post_lambda)
 
         with mailgun_api, mailgun_domain, post_request, allow_emailing:
             mailgun_email_services.send_mail(
@@ -84,19 +106,21 @@ def test_send_mail_data_properly_sent(self):
     def test_bcc_admin_flag(self):
         """Verifies that the bcc admin flag is working properly in send_mail.
 
-        Note that we replace the requests.post() function in send_mail with
-        an alternate lambda that asserts the correct values were placed in
-        the data dictionary that is then passed to the mailgun api.
+        Note that we replace the mailgun_email_services.post_to_mailgun()
+        function in send_mail with an alternate lambda that asserts the correct
+        values were placed in the data dictionary that is then passed to the
+        mailgun api.
         """
         mailgun_api = self.swap(feconf, 'MAILGUN_API_KEY', 'api')
         mailgun_domain = self.swap(feconf, 'MAILGUN_DOMAIN_NAME', 'domain')
         allow_emailing = self.swap(feconf, 'CAN_SEND_EMAILS', True)
 
-        # Lambda function, will replace requests.post() in send_mail.
-        req_post_lambda = (lambda domain_name, auth=None, data=None:
+        # Lambda function, will replace post_to_mailgun().
+        req_post_lambda = (lambda data=None:
                            self.assertEqual(
                                data['bcc'], feconf.ADMIN_EMAIL_ADDRESS))
-        post_request = self.swap(requests, 'post', req_post_lambda)
+        post_request = self.swap(
+            mailgun_email_services, 'post_to_mailgun', req_post_lambda)
 
         with mailgun_api, mailgun_domain, post_request, allow_emailing:
             mailgun_email_services.send_mail(
@@ -110,12 +134,13 @@ def test_reply_to_id_flag(self):
         allow_emailing = self.swap(feconf, 'CAN_SEND_EMAILS', True)
         reply_id = 123
 
-        # Lambda function, will replace requests.post() in send_mail.
-        req_post_lambda = (lambda domain_name, auth=None, data=None:
+        # Lambda function, will replace post_to_mailgun().
+        req_post_lambda = (lambda data=None:
                            self.assertEqual(data['h:Reply-To'],
                                             'reply+' + str(reply_id) + '@' +
                                             feconf.INCOMING_EMAILS_DOMAIN_NAME))
-        post_request = self.swap(requests, 'post', req_post_lambda)
+        post_request = self.swap(
+            mailgun_email_services, 'post_to_mailgun', req_post_lambda)
 
         with mailgun_api, mailgun_domain, post_request, allow_emailing:
             mailgun_email_services.send_mail(
@@ -130,7 +155,8 @@ def test_send_bulk_mail_raises_exception_for_missing_api_key(self):
         mailgun_api_exception = (
             self.assertRaisesRegexp(
                 Exception, 'Mailgun API key is not available.'))
-        with mailgun_api_exception:
+        allow_emailing = self.swap(feconf, 'CAN_SEND_EMAILS', True)
+        with mailgun_api_exception, allow_emailing:
             mailgun_email_services.send_bulk_mail(
                 feconf.SYSTEM_EMAIL_ADDRESS, [feconf.ADMIN_EMAIL_ADDRESS],
                 'subject', 'body', 'html')
@@ -140,10 +166,11 @@ def test_send_bulk_mail_raises_exception_for_missing_domain_name(self):
             send_bulk_mail.
         """
         mailgun_api = self.swap(feconf, 'MAILGUN_API_KEY', 'api')
+        allow_emailing = self.swap(feconf, 'CAN_SEND_EMAILS', True)
         mailgun_domain_name_exception = (
             self.assertRaisesRegexp(
                 Exception, 'Mailgun domain name is not set.'))
-        with mailgun_api, mailgun_domain_name_exception:
+        with mailgun_api, mailgun_domain_name_exception, allow_emailing:
             mailgun_email_services.send_bulk_mail(
                 feconf.SYSTEM_EMAIL_ADDRESS, [feconf.ADMIN_EMAIL_ADDRESS],
                 'subject', 'body', 'html')
@@ -171,16 +198,16 @@ def test_send_bulk_mail_data_properly_sent(self):
         allow_emailing = self.swap(feconf, 'CAN_SEND_EMAILS', True)
         recipients = [feconf.ADMIN_EMAIL_ADDRESS]
 
-        # Data that we expect to have been sent in the requests.post().
+        # Data that we expect to have been sent in the post_to_mailgun().
         expected = ({'from': feconf.SYSTEM_EMAIL_ADDRESS, 'to': recipients,
                      'subject': 'subject', 'text': 'body', 'html': 'html',
                      'recipient-variables': '{}'})
 
-        # Lambda function, will replace requests.post() in send_mail.
-        req_post_lambda = (lambda domain_name, auth=None, data=None:
+        # Lambda function, will replace post_to_mailgun().
+        req_post_lambda = (lambda data=None:
                            self.assertDictContainsSubset(expected, data))
-
-        post_request = self.swap(requests, 'post', req_post_lambda)
+        post_request = self.swap(
+            mailgun_email_services, 'post_to_mailgun', req_post_lambda)
 
         with mailgun_api, mailgun_domain, post_request, allow_emailing:
             mailgun_email_services.send_bulk_mail(
diff --git a/core/tests/gae_suite.py b/core/tests/gae_suite.py
--- a/core/tests/gae_suite.py
+++ b/core/tests/gae_suite.py
@@ -52,7 +52,6 @@
     os.path.join(THIRD_PARTY_DIR, 'graphy-1.0.0'),
     os.path.join(THIRD_PARTY_DIR, 'html5lib-python-1.0.1'),
     os.path.join(THIRD_PARTY_DIR, 'mutagen-1.42.0'),
-    os.path.join(THIRD_PARTY_DIR, 'requests-2.10.0'),
     os.path.join(THIRD_PARTY_DIR, 'simplejson-3.16.0'),
     os.path.join(THIRD_PARTY_DIR, 'six-1.12.0'),
     os.path.join(THIRD_PARTY_DIR, 'soupsieve-1.9.1'),
diff --git a/scripts/backend_tests.py b/scripts/backend_tests.py
--- a/scripts/backend_tests.py
+++ b/scripts/backend_tests.py
@@ -63,7 +63,6 @@
     os.path.join(THIRD_PARTY_DIR, 'graphy-1.0.0'),
     os.path.join(THIRD_PARTY_DIR, 'html5lib-python-1.0.1'),
     os.path.join(THIRD_PARTY_DIR, 'mutagen-1.42.0'),
-    os.path.join(THIRD_PARTY_DIR, 'requests-2.22.0'),
     os.path.join(THIRD_PARTY_DIR, 'simplejson-3.16.0'),
     os.path.join(THIRD_PARTY_DIR, 'six-1.12.0'),
     os.path.join(THIRD_PARTY_DIR, 'soupsieve-1.9.1'),