Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 102 additions & 99 deletions app/routes/sentiment_routes.py
Original file line number Diff line number Diff line change
@@ -1,99 +1,102 @@
"""
This module contains the routes for the sentiment endpoint.
"""

from flask_restx import Namespace, Resource, fields
from flask import request

from app.utils.logger import logger

# Services
from app.services.sentiment_service import SentimentService

service = SentimentService()

def register_routes(api):
# Define the model for the sentiment analysis request body
sentiment_analyze_request_model = api.model('SentimentAnalyzeRequestModel', {
'text': fields.String(required=True, description='Input text for sentiment analysis.', example='I love this product!')
})

sentiment_analyze_bad_request_model = api.model('SentimentAnalyzeBadRequestModel', {
'status': fields.String(required=True, description='The status of the response', example='error'),
'error': fields.String(required=True, description='The error message', example='text is required'),
'data': fields.Raw(description='Data field will be null for error responses', example=None)
})

sentiment_analyze_internal_server_error_model = api.model('SentimentAnalyzeInternalServerErrorModel', {
'status': fields.String(required=True, description='The status of the response', example='error'),
'error': fields.String(required=True, description='The error message', example='An unexpected error occurred during sentiment analysis.'),
'data': fields.Raw(description='Data field will be null for error responses', example=None)
})

sentiment_analyze_success_model = api.model('SentimentAnalyzeSuccessModel', {
'status': fields.String(required=True, description='The status of the response', example='success'),
'data': fields.Nested(api.model('SentimentAnalyzeDataModel', {
'label': fields.String(required=True, description='Predicted sentiment label.', enum=['POS', 'NEG', 'NEU'], example='POS'),
'confidence': fields.Float(required=True, description='Confidence score of the prediction.', example=0.95)
})) # Embed the data model
})

# Define the endpoint for the Analyze sentiment of a text.
@api.route('/analyze')
class SentimentAnalyze(Resource):
@api.doc(description="Analyze sentiment of a text.")
@api.expect(sentiment_analyze_request_model) # Use the model for request validation
@api.response(200, 'Success', sentiment_analyze_success_model)
@api.response(400, 'Bad Request', sentiment_analyze_bad_request_model)
@api.response(500, 'Internal Server Error', sentiment_analyze_internal_server_error_model)
def post(self):
"""
Endpoint to analyze sentiment of a text.
- text (str): Input text for sentiment analysis.
"""
try:
# Parse the request body
data = request.json

text = data.get('text')

if not text:
return {
'status': 'error',
'error': 'text is required.',
'data': None
}, 400

# Call the service to analyze the sentiment of the text
result = service.analyze(text)

if 'error' in result:
return {
'status': 'error',
'error': result['error'],
'data': None
}, 500 # Internal Server Error

# Return the predicted label and confidence score
return {
'status': 'success',
'data': {
'label': result['label'],
'confidence': result['confidence']
}
}

except Exception as e:
logger.error(f"[error] [Route Layer] [SentimentAnalyze] [post] An error occurred: {str(e)}")
# print(f"[error] [Route Layer] [SentimentAnalyze] [post] An error occurred: {str(e)}")
return {
'status': 'error',
"error": 'An unexpected error occurred while processing the request.', # Generic error message
'data': None
}, 500 # Internal Server Error

# Define the namespace for the sentiment endpoint
api = Namespace('Sentiment', description='Sentiment Operations')

# Register the routes
register_routes(api)
"""
This module contains the routes for the sentiment endpoint.
"""

from flask_restx import Namespace, Resource, fields
from flask import request

from app.utils.logger import logger

# Services
from app.services.sentiment_service import SentimentService

service = SentimentService()

def register_routes(api):
# Define the model for the sentiment analysis request body
sentiment_analyze_request_model = api.model('SentimentAnalyzeRequestModel', {
'text': fields.String(required=True, description='Input text for sentiment analysis.', example='I love this product!')
})

sentiment_analyze_bad_request_model = api.model('SentimentAnalyzeBadRequestModel', {
'status': fields.String(required=True, description='The status of the response', example='error'),
'error': fields.String(required=True, description='The error message', example='text is required'),
'data': fields.Raw(description='Data field will be null for error responses', example=None)
})

sentiment_analyze_internal_server_error_model = api.model('SentimentAnalyzeInternalServerErrorModel', {
'status': fields.String(required=True, description='The status of the response', example='error'),
'error': fields.String(required=True, description='The error message', example='An unexpected error occurred during sentiment analysis.'),
'data': fields.Raw(description='Data field will be null for error responses', example=None)
})

sentiment_analyze_success_model = api.model('SentimentAnalyzeSuccessModel', {
'status': fields.String(required=True, description='The status of the response', example='success'),
'data': fields.Nested(api.model('SentimentAnalyzeDataModel', {
'label': fields.String(required=True, description='Predicted sentiment label.', enum=['POS', 'NEG', 'NEU'], example='POS'),
'confidence': fields.Float(required=True, description='Confidence score of the prediction.', example=0.95)
}))
})

@api.route('/analyze')
class SentimentAnalyze(Resource):
@api.doc(description="Analyze sentiment of a text.")
@api.expect(sentiment_analyze_request_model)
@api.response(200, 'Success', sentiment_analyze_success_model)
@api.response(400, 'Bad Request', sentiment_analyze_bad_request_model)
@api.response(500, 'Internal Server Error', sentiment_analyze_internal_server_error_model)
def post(self):
"""
Endpoint to analyze sentiment of a text.
- text (str): Input text for sentiment analysis.
"""
try:
data = request.json

text = data.get('text')

# Reject missing, empty, or whitespace-only text.
# The original guard `if not text` passed whitespace-only
# strings (e.g. " ") straight to the model as valid input.
if not text or not text.strip():
return {
'status': 'error',
'error': 'text is required.',
'data': None
}, 400

# Call the service to analyze the sentiment of the text
result = service.analyze(text)

if 'error' in result:
return {
'status': 'error',
'error': result['error'],
'data': None
}, 500

return {
'status': 'success',
'data': {
'label': result['label'],
'confidence': result['confidence']
}
}

except Exception as e:
logger.error(
"[Route Layer] [SentimentAnalyze] [post] An error occurred: %s",
str(e)
)
return {
'status': 'error',
'error': 'An unexpected error occurred while processing the request.',
'data': None
}, 500


# Define the namespace for the sentiment endpoint
api = Namespace('Sentiment', description='Sentiment Operations')

# Register the routes
register_routes(api)
36 changes: 35 additions & 1 deletion tests/unit/test_routes/test_sentiment_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,38 @@ def test_sentiment_analyze_success(self, mock_analyze):
mock_analyze.assert_called_once_with(payload['text'])

# # Run:
# coverage run -m pytest .\tests\unit\test_routes\test_sentiment_routes.py
# coverage run -m pytest .\tests\unit\test_routes\test_sentiment_routes.py

def test_sentiment_analyze_whitespace_only_text(self, mock_analyze):
"""
Test that whitespace-only text is rejected with a 400 error.
The original guard `if not text` passed strings like " " straight
to the model as valid input. The fix adds `.strip()` to catch this.
"""
payload = {"text": " "}
response = self.client.post(self.endpoint, json=payload)

assert response.status_code == 400
assert response.json == {
"status": "error",
"error": "text is required.",
"data": None
}
mock_analyze.assert_not_called()

def test_sentiment_analyze_empty_string_text(self, mock_analyze):
"""
Test that an empty string is rejected with a 400 error.
Complements the whitespace test — verifies both empty and
whitespace-only strings are caught by the same guard clause.
"""
payload = {"text": ""}
response = self.client.post(self.endpoint, json=payload)

assert response.status_code == 400
assert response.json == {
"status": "error",
"error": "text is required.",
"data": None
}
mock_analyze.assert_not_called()