changeset 8:6541622b6127

add tweet analysis method
author Dennis Concepcion Martin <dennisconcepcionmartin@gmail.com>
date Fri, 17 Sep 2021 21:10:02 +0200
parents 1b1296559c31
children c16b47541947
files .aws-sam/build.toml README.md dependencies/python/aws_controller.py dependencies/python/event_controller.py dependencies/python/secrets_controller.py dependencies/python/url_controller.py src/handlers/sentiment.py src/handlers/tweet_sentiment_handler.py src/requirements.txt template.yaml tests/unit/test_event_controller.py tests/unit/test_url_controller.py
diffstat 12 files changed, 178 insertions(+), 130 deletions(-) [+]
line wrap: on
line diff
--- a/.aws-sam/build.toml	Fri Sep 17 17:42:54 2021 +0200
+++ b/.aws-sam/build.toml	Fri Sep 17 21:10:02 2021 +0200
@@ -1,11 +1,5 @@
 # This file is auto generated by SAM CLI build command
 
 [function_build_definitions]
-[function_build_definitions.631c5dbb-3ad7-4033-889a-2b55c4ab69a0]
-codeuri = "/Users/dennis/Developer/tweet-analysis/handlers"
-runtime = "python3.9"
-source_md5 = ""
-packagetype = "Zip"
-functions = ["GetTweetSentimentFunction"]
 
 [layer_build_definitions]
--- a/README.md	Fri Sep 17 17:42:54 2021 +0200
+++ b/README.md	Fri Sep 17 21:10:02 2021 +0200
@@ -1,10 +1,11 @@
 # tweet-analysis
+Serverless application to fetch & analyse tweets using AWS Comprehend.
 
 ## Structure
 
 - src - Code for the application's Lambda function.
 - events - Invocation events that you can use to invoke the function.
-- tests - Unit tests for the application code. 
+- tests - Unit and integration tests for the application code. 
 - template.yaml - A template that defines the application's AWS resources.
 
 The application uses several AWS resources, including Lambda functions and an API Gateway API. 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dependencies/python/aws_controller.py	Fri Sep 17 21:10:02 2021 +0200
@@ -0,0 +1,71 @@
+import boto3
+import base64
+import json
+from botocore.exceptions import ClientError
+
+
+class AwsSecretsManager:
+    @staticmethod
+    def get_secret(secret_name, region_name='eu-west-2'):
+        """
+        Get Secret Keys from AWS Secrets Manager
+        :return:
+        """
+
+        # Create a Secrets Manager client
+        session = boto3.session.Session()
+        client = session.client(
+            service_name='secretsmanager',
+            region_name=region_name
+        )
+
+        try:
+            get_secret_value_response = client.get_secret_value(SecretId=secret_name)
+        except ClientError as e:
+            if e.response['Error']['Code'] == 'DecryptionFailureException':
+                # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
+                raise e
+            elif e.response['Error']['Code'] == 'InternalServiceErrorException':
+                # An error occurred on the server side.
+                raise e
+            elif e.response['Error']['Code'] == 'InvalidParameterException':
+                # You provided an invalid value for a parameter.
+                raise e
+            elif e.response['Error']['Code'] == 'InvalidRequestException':
+                # You provided a parameter value that is not valid for the current state of the resource.
+                raise e
+            elif e.response['Error']['Code'] == 'ResourceNotFoundException':
+                # AWS Can't find the resource that you asked for.
+                raise e
+        else:
+            # Decrypts secret using the associated KMS CMK.
+            # Depending on whether the secret is a string or binary, one of these fields will be populated.
+            if 'SecretString' in get_secret_value_response:
+                secret = get_secret_value_response['SecretString']
+            else:
+                secret = base64.b64decode(get_secret_value_response['SecretBinary'])
+
+            return json.loads(secret)
+
+
+class AwsComprehend:
+
+    @staticmethod
+    def get_sentiment(tweets):
+        """
+        :param tweets: list (string), required
+        :return: dict
+        """
+        # Create a Comprehend client
+        session = boto3.session.Session()
+        comprehend = session.client(
+            service_name='comprehend',
+            region_name='eu-west-2'
+        )
+
+        response = comprehend.batch_detect_sentiment(
+            TextList=tweets,
+            LanguageCode='en'
+        )
+
+        return response
--- a/dependencies/python/event_controller.py	Fri Sep 17 17:42:54 2021 +0200
+++ b/dependencies/python/event_controller.py	Fri Sep 17 21:10:02 2021 +0200
@@ -1,24 +1,27 @@
-def unwrap_sentiment_string_parameters(event):
-    """
-    Unwrap string parameters from /sentiment api call
-    :param event: dict, required
-        API Gateway Lambda Proxy Input Format
-    :return:
-    """
+class SentimentFunctionEvent:
 
-    twitter_user = 'Twitter'
-    number_of_tweets = '100'
+    @staticmethod
+    def unwrap_parameters(event):
+        """
+        Unwrap string parameters from /sentiment api call
+        :param event: dict, required
+            API Gateway Lambda Proxy Input Format
+        :return:
+        """
+
+        twitter_user = 'Twitter'
+        number_of_tweets = '100'
 
-    query_string_parameters = event['queryStringParameters']
-    if event['queryStringParameters'] is not None:
-        if 'twitterUser' in query_string_parameters:
-            twitter_user = query_string_parameters['twitterUser']
-            if not twitter_user:
-                twitter_user = 'Twitter'
+        query_string_parameters = event['queryStringParameters']
+        if event['queryStringParameters'] is not None:
+            if 'twitterUser' in query_string_parameters:
+                twitter_user = query_string_parameters['twitterUser']
+                if not twitter_user:
+                    twitter_user = 'Twitter'
 
-        if 'numberOfTweets' in query_string_parameters:
-            number_of_tweets = query_string_parameters['numberOfTweets']
-            if not number_of_tweets:
-                number_of_tweets = '100'
+            if 'numberOfTweets' in query_string_parameters:
+                number_of_tweets = query_string_parameters['numberOfTweets']
+                if not number_of_tweets:
+                    number_of_tweets = '100'
 
-    return twitter_user, number_of_tweets
+        return twitter_user, number_of_tweets
--- a/dependencies/python/secrets_controller.py	Fri Sep 17 17:42:54 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-import boto3
-import base64
-import json
-from botocore.exceptions import ClientError
-
-
-def get_secret(secret_name, region_name="eu-west-2"):
-    """
-    Get Secret Keys from AWS Secrets Manager
-    :return:
-    """
-
-    # Create a Secrets Manager client
-    session = boto3.session.Session()
-    client = session.client(
-        service_name='secretsmanager',
-        region_name=region_name
-    )
-
-    try:
-        get_secret_value_response = client.get_secret_value(SecretId=secret_name)
-    except ClientError as e:
-        if e.response['Error']['Code'] == 'DecryptionFailureException':
-            # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
-            raise e
-        elif e.response['Error']['Code'] == 'InternalServiceErrorException':
-            # An error occurred on the server side.
-            raise e
-        elif e.response['Error']['Code'] == 'InvalidParameterException':
-            # You provided an invalid value for a parameter.
-            raise e
-        elif e.response['Error']['Code'] == 'InvalidRequestException':
-            # You provided a parameter value that is not valid for the current state of the resource.
-            raise e
-        elif e.response['Error']['Code'] == 'ResourceNotFoundException':
-            # AWS Can't find the resource that you asked for.
-            raise e
-    else:
-        # Decrypts secret using the associated KMS CMK.
-        # Depending on whether the secret is a string or binary, one of these fields will be populated.
-        if 'SecretString' in get_secret_value_response:
-            secret = get_secret_value_response['SecretString']
-        else:
-            secret = base64.b64decode(get_secret_value_response['SecretBinary'])
-
-        return json.loads(secret)
--- a/dependencies/python/url_controller.py	Fri Sep 17 17:42:54 2021 +0200
+++ b/dependencies/python/url_controller.py	Fri Sep 17 21:10:02 2021 +0200
@@ -1,13 +1,15 @@
-def create_twitter_url(twitter_user, number_of_tweets):
-    """
-    Create url to fetch `max_results` of tweets from `user`
-    :param twitter_user: string, required
-    :param number_of_tweets: int, required
-    :return: string url
-    """
+class TwitterApi:
 
-    formatted_max_results = 'max_results={}'.format(number_of_tweets)
-    formatted_user = 'query=from:{}'.format(twitter_user)
-    url = "https://api.twitter.com/2/tweets/search/recent?{}&{}".format(formatted_max_results, formatted_user)
+    @staticmethod
+    def create_sentiment_url(twitter_user, number_of_tweets):
+        """
+        Create url to fetch `max_results` of tweets from `user`
+        :param twitter_user: string, required
+        :param number_of_tweets: int, required
+        :return: string url
+        """
 
-    return url
+        query = 'query=from:{}'.format(twitter_user)
+        url = 'https://api.twitter.com/2/tweets/search/recent?max_results={}&{}'.format(number_of_tweets, query)
+
+        return url
--- a/src/handlers/sentiment.py	Fri Sep 17 17:42:54 2021 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-import requests
-
-# noinspection PyUnresolvedReferences
-from secrets_controller import get_secret
-# noinspection PyUnresolvedReferences
-from event_controller import unwrap_sentiment_string_parameters
-# noinspection PyUnresolvedReferences
-from url_controller import create_twitter_url
-
-
-def get_tweet_sentiment(event, context):
-    """
-    :param event: dict, required
-        API Gateway Lambda Proxy Input Format
-    :param context: object, required
-        Lambda Context runtime methods and attributes
-    :return: dict
-        API Gateway Lambda Proxy Output Format
-    """
-
-    # Unwrap query string parameters
-    twitter_user, number_of_tweets = unwrap_sentiment_string_parameters(event)
-
-    # URL creation & authentication
-    twitter_url = create_twitter_url(twitter_user, number_of_tweets)
-    twitter_key = get_secret(secret_name='tweet-analysis-keys')
-    twitter_header = {"Authorization": "Bearer {}".format(twitter_key['BEARER'])}
-
-    # Request tweets to Twitter
-    twitter_response = requests.request("GET", twitter_url, headers=twitter_header)
-
-    # Analyse tweets with AWS Comprehend
-
-    return {
-        "statusCode": 200,
-        "body": {
-            "tweets": twitter_response.json()
-        }
-    }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/handlers/tweet_sentiment_handler.py	Fri Sep 17 21:10:02 2021 +0200
@@ -0,0 +1,47 @@
+import json
+import requests
+
+# noinspection PyUnresolvedReferences
+from aws_controller import AwsSecretsManager, AwsComprehend
+# noinspection PyUnresolvedReferences
+from event_controller import SentimentFunctionEvent
+# noinspection PyUnresolvedReferences
+from url_controller import TwitterApi
+
+
+# noinspection PyUnusedLocal
+def get_tweet_sentiment(event, context):
+    """
+    :param event: dict, required
+        API Gateway Lambda Proxy Input Format
+    :param context: object, required
+        Lambda Context runtime methods and attributes
+    :return: dict
+        API Gateway Lambda Proxy Output Format
+    """
+
+    # Unwrap query string parameters
+    twitter_user, number_of_tweets = SentimentFunctionEvent.unwrap_parameters(event)
+
+    # URL creation & authentication
+    twitter_url = TwitterApi.create_sentiment_url(twitter_user, number_of_tweets)
+    twitter_key = AwsSecretsManager.get_secret(secret_name='tweet-analysis-keys')
+    twitter_header = {"Authorization": "Bearer {}".format(twitter_key['BEARER'])}
+
+    # Request tweets to Twitter
+    twitter_response = requests.request("GET", twitter_url, headers=twitter_header)
+    twitter_json_response = twitter_response.json()
+
+    tweets = []
+    for tweet in twitter_json_response['data']['tweets']:
+        tweets.append(tweet['text'])
+
+    # Analyse tweets with AWS Comprehend
+    result = AwsComprehend.get_sentiment(tweets)
+
+    return {
+        "statusCode": 200,
+        "body": {
+            "tweets": json.dumps(result)
+        }
+    }
--- a/src/requirements.txt	Fri Sep 17 17:42:54 2021 +0200
+++ b/src/requirements.txt	Fri Sep 17 21:10:02 2021 +0200
@@ -1,1 +1,2 @@
-requests
\ No newline at end of file
+requests
+boto3
\ No newline at end of file
--- a/template.yaml	Fri Sep 17 17:42:54 2021 +0200
+++ b/template.yaml	Fri Sep 17 21:10:02 2021 +0200
@@ -152,13 +152,13 @@
   ##
 
   # Define lambda functions
-  GetTweetSentimentFunction:
+  TweetSentimentHandler:
     Type: AWS::Serverless::Function
     Properties:
       FunctionName: GetTweetSentimentFunction
       Description: Fetch tweets and analyse sentiment using AWS Comprehend
       CodeUri: src/
-      Handler: handlers/sentiment.get_tweet_sentiment
+      Handler: handlers/tweet_sentiment_handler.get_tweet_sentiment
       Runtime: python3.9
       Policies:
         - AWSSecretsManagerGetSecretValuePolicy:
--- a/tests/unit/test_event_controller.py	Fri Sep 17 17:42:54 2021 +0200
+++ b/tests/unit/test_event_controller.py	Fri Sep 17 21:10:02 2021 +0200
@@ -1,8 +1,8 @@
 from unittest import TestCase
-from dependencies.python.event_controller import *
+from dependencies.python.event_controller import SentimentFunctionEvent
 
 
-class TestUnwrapStringParameters(TestCase):
+class TestSentimentFunctionEvent(TestCase):
 
     @staticmethod
     def create_event(query_string_parameter):
@@ -16,7 +16,7 @@
 
         return event
 
-    def test_unwrap_sentiment_string_parameters(self):
+    def test_unwrap_parameters(self):
         test_cases = {
             '1': None,
             '2': {'twitterUser': ''},
@@ -37,7 +37,7 @@
 
         for test_number in test_cases:
             event = self.create_event(test_cases[test_number])
-            twitter_user, number_of_tweets = unwrap_sentiment_string_parameters(event)
+            twitter_user, number_of_tweets = SentimentFunctionEvent.unwrap_parameters(event)
             expected_twitter_user = expected_results[test_number]['twitterUser']
             expected_number_of_tweets = expected_results[test_number]['numberOfTweets']
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/unit/test_url_controller.py	Fri Sep 17 21:10:02 2021 +0200
@@ -0,0 +1,14 @@
+from unittest import TestCase
+from dependencies.python.url_controller import TwitterApi
+
+
+class TestTwitterApi(TestCase):
+    def test_create_twitter_url(self):
+        twitter_user = 'Twitter'
+        number_of_tweets = '50'
+        url = TwitterApi.create_sentiment_url(twitter_user, number_of_tweets)
+        expected_url = 'https://api.twitter.com/2/tweets/search/recent?max_results={}&query=from:{}'.format(
+            number_of_tweets, twitter_user
+        )
+
+        self.assertEqual(url, expected_url)