Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send email endpoint #37

Merged
merged 5 commits into from
Jul 1, 2021
Merged
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
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__pycache__
.coverage
Makefile
README.md
.github/
LICENCE
config/sample.env
config/current.env
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.coverage
__pycache__
config/current.env
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ run-command=(podman run --rm -ti --volume $(PWD):/mnt/code:rw \
--env QUERIDO_DIARIO_DATABASE_CSV=$(QUERIDO_DIARIO_DATABASE_CSV) \
--env PYTHONPATH=/mnt/code \
--env RUN_INTEGRATION_TESTS=$(RUN_INTEGRATION_TESTS) \
--env-file config/current.env \
--user=$(UID):$(UID) $(IMAGE_NAMESPACE)/$(IMAGE_NAME):$(IMAGE_TAG) $1)

wait-for=(podman run --rm -ti --volume $(PWD):/mnt/code:rw \
Expand Down Expand Up @@ -68,6 +69,7 @@ destroy-pod:
podman pod rm --force --ignore $(POD_NAME)

create-pod: destroy-pod
cp --no-clobber config/sample.env config/current.env
podman pod create --publish $(API_PORT):$(API_PORT) \
--publish $(ELASTICSEARCH_PORT1):$(ELASTICSEARCH_PORT1) \
--publish $(ELASTICSEARCH_PORT2):$(ELASTICSEARCH_PORT2) \
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ need to insert data into the database. There is another make target, `make apisq
which open the `psql` and connect to the database. Thus, you can insert data
using some `INSERT INTO ...` statements and test the API. ;)


### Using suggestion endpoint

You need to create a token at [Mailjet](www.mailjet.com) to run
application and send email (put on `config/current.env`).
andreformento marked this conversation as resolved.
Show resolved Hide resolved

## Tests

The project uses TDD during development. This means that there are no changes
Expand Down
55 changes: 51 additions & 4 deletions api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from datetime import date
from typing import List, Optional

from fastapi import FastAPI, Query, Path
from fastapi import FastAPI, Query, Path, Response, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field

from gazettes import GazetteAccessInterface, GazetteRequest
from suggestions import Suggestion, SuggestionServiceInterface
from config.config import load_configuration

config = load_configuration()
Expand Down Expand Up @@ -79,6 +80,22 @@ class HTTPExceptionMessage(BaseModel):
detail: str


class CreateSuggestionBody(BaseModel):
email_address: str = Field(
title="Email address", description="Email address who is sending email"
)
name: str = Field(
title="Name", description="Name who is sending email",
)
content: str = Field(
title="Email content", description="Email content with suggestion",
)


class CreatedSuggestionResponse(BaseModel):
status: str


def trigger_gazettes_search(
territory_id: str = None,
since: date = None,
Expand Down Expand Up @@ -295,10 +312,40 @@ async def get_city(territory_id: str = Path(..., description="City's IBGE ID")):
return {"city": city_info}


def configure_api_app(gazettes: GazetteAccessInterface, api_root_path=None):
@app.post(
"/suggestions",
response_model=CreatedSuggestionResponse,
name="Send a suggestion",
description="Send a suggestion to the project",
response_model_exclude_unset=True,
response_model_exclude_none=True,
)
async def add_suggestion(response: Response, body: CreateSuggestionBody):
suggestion = Suggestion(
email_address=body.email_address, name=body.name, content=body.content,
)
suggestion_sent = app.suggestion_service.add_suggestion(suggestion)
response.status_code = (
status.HTTP_200_OK if suggestion_sent.success else status.HTTP_400_BAD_REQUEST
)
return {"status": suggestion_sent.status}


def configure_api_app(
gazettes: GazetteAccessInterface,
suggestion_service: SuggestionServiceInterface,
api_root_path=None,
):
if not isinstance(gazettes, GazetteAccessInterface):
raise Exception("Only GazetteAccessInterface object are accepted")
raise Exception(
"Only GazetteAccessInterface object are accepted for gazettes parameter"
)
if api_root_path is not None and type(api_root_path) != str:
raise Exception("Invalid api_root_path")
if not isinstance(suggestion_service, SuggestionServiceInterface):
raise Exception(
"Only SuggestionServiceInterface object are accepted for suggestion_service parameter"
)
app.gazettes = gazettes
app.suggestion_service = suggestion_service
app.root_path = api_root_path
21 changes: 21 additions & 0 deletions config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ def __init__(self):
self.cors_allow_headers = Configuration._load_list(
"QUERIDO_DIARIO_CORS_ALLOW_HEADERS", ["*"]
)
self.suggestion_mailjet_rest_api_key = os.environ.get(
"QUERIDO_DIARIO_SUGGESTION_MAILJET_REST_API_KEY", ""
)
self.suggestion_mailjet_rest_api_secret = os.environ.get(
"QUERIDO_DIARIO_SUGGESTION_MAILJET_REST_API_SECRET", ""
)
self.suggestion_sender_name = os.environ.get(
"QUERIDO_DIARIO_SUGGESTION_SENDER_NAME", ""
)
self.suggestion_sender_email = os.environ.get(
"QUERIDO_DIARIO_SUGGESTION_SENDER_EMAIL", ""
)
self.suggestion_recipient_name = os.environ.get(
"QUERIDO_DIARIO_SUGGESTION_RECIPIENT_NAME", ""
)
self.suggestion_recipient_email = os.environ.get(
"QUERIDO_DIARIO_SUGGESTION_RECIPIENT_EMAIL", ""
)
self.suggestion_mailjet_custom_id = os.environ.get(
"QUERIDO_DIARIO_SUGGESTION_MAILJET_CUSTOM_ID", ""
)

@classmethod
def _load_list(cls, key, default=[]):
Expand Down
7 changes: 7 additions & 0 deletions config/sample.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
QUERIDO_DIARIO_SUGGESTION_MAILJET_REST_API_KEY=mailjet.com
QUERIDO_DIARIO_SUGGESTION_MAILJET_REST_API_SECRET=mailjet.com
QUERIDO_DIARIO_SUGGESTION_SENDER_NAME=Sender Name
[email protected]
QUERIDO_DIARIO_SUGGESTION_RECIPIENT_NAME=Recipient Name
[email protected]
QUERIDO_DIARIO_SUGGESTION_MAILJET_CUSTOM_ID=AppCustomID
12 changes: 11 additions & 1 deletion main/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@
from index import create_elasticsearch_data_mapper
from config import load_configuration
from database import create_database_interface
from suggestions import create_suggestion_service

configuration = load_configuration()
datagateway = create_elasticsearch_data_mapper(configuration.host, configuration.index)
database = create_database_interface()
gazettes_interface = create_gazettes_interface(datagateway, database)
configure_api_app(gazettes_interface, configuration.root_path)
suggestion_service = create_suggestion_service(
suggestion_mailjet_rest_api_key=configuration.suggestion_mailjet_rest_api_key,
suggestion_mailjet_rest_api_secret=configuration.suggestion_mailjet_rest_api_secret,
suggestion_sender_name=configuration.suggestion_sender_name,
suggestion_sender_email=configuration.suggestion_sender_email,
suggestion_recipient_name=configuration.suggestion_recipient_name,
suggestion_recipient_email=configuration.suggestion_recipient_email,
suggestion_mailjet_custom_id=configuration.suggestion_mailjet_custom_id,
)
configure_api_app(gazettes_interface, suggestion_service, configuration.root_path)

uvicorn.run(app, host="0.0.0.0", port=8080, root_path=configuration.root_path)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ uvicorn==0.11.8
psycopg2==2.8.5
SQLAlchemy==1.3.19
elasticsearch==7.9.1
mailjet-rest==1.3.4
10 changes: 10 additions & 0 deletions suggestions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .model import (
Suggestion,
SuggestionSent,
)

from .service import (
SuggestionServiceInterface,
MailjetSuggestionService, # only for test
create_suggestion_service,
)
45 changes: 45 additions & 0 deletions suggestions/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
class Suggestion:
"""
Object containing the data to suggest
"""

def __init__(
self, email_address, name, content,
):
self.email_address = email_address
self.name = name
self.content = content

def __hash__(self):
return hash((self.email_address, self.name, self.content,))

def __eq__(self, other):
return (
self.email_address == other.email_address
and self.name == other.name
and self.content == other.content
)

def __repr__(self):
return f"Suggestion({self.email_address}, {self.name}, {self.content})"


class SuggestionSent:
"""
Object containing the suggestion sent
"""

def __init__(
self, success, status,
):
self.success = success
self.status = status

def __hash__(self):
return hash((self.success, self.status,))

def __eq__(self, other):
return self.success == other.success and self.status == other.status

def __repr__(self):
return f"SuggestionSent({self.success}, {self.status})"
108 changes: 108 additions & 0 deletions suggestions/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import abc
import logging

from mailjet_rest import Client

from .model import (
Suggestion,
SuggestionSent,
)


class SuggestionServiceInterface(abc.ABC):
"""
Service to send a suggestion
"""

@abc.abstractmethod
def add_suggestion(self, suggestion: Suggestion):
"""
Method to send a suggestion
"""


class MailjetSuggestionService(SuggestionServiceInterface):
def __init__(
self,
mailjet_client: Client,
suggestion_sender_name: str,
suggestion_sender_email: str,
suggestion_recipient_name: str,
suggestion_recipient_email: str,
suggestion_mailjet_custom_id: str,
):
self.mailjet_client = mailjet_client
self.suggestion_sender_name = suggestion_sender_name
self.suggestion_sender_email = suggestion_sender_email
self.suggestion_recipient_name = suggestion_recipient_name
self.suggestion_recipient_email = suggestion_recipient_email
self.suggestion_mailjet_custom_id = suggestion_mailjet_custom_id
self.logger = logging.getLogger(__name__)

def add_suggestion(self, suggestion: Suggestion):
data = {
"Messages": [
{
"From": {
"Name": self.suggestion_sender_name,
"Email": self.suggestion_sender_email,
},
"To": [
{
"Name": self.suggestion_recipient_name,
"Email": self.suggestion_recipient_email,
}
],
"Subject": "Querido Diário, hoje recebi uma sugestão",
"TextPart": f"From {suggestion.name} <{suggestion.email_address}>:\n\n{suggestion.content}",
"CustomID": self.suggestion_mailjet_custom_id,
}
]
}
result = self.mailjet_client.send.create(data=data)
result_json = result.json()

self.logger.debug(f"Suggestion body response {result_json}")
if 200 <= result.status_code <= 299:
self.logger.info(f"Suggestion created for {suggestion.email_address}")
return SuggestionSent(success=True, status="Sent")
else:
status = "unknown error"
try:
errors = []
for message in result_json["Messages"]:
for error in message["Errors"]:
errors.append(error["ErrorMessage"])
if errors:
status = ", ".join(errors)
except KeyError:
pass

self.logger.error(
f"Could not sent message to <{suggestion.email_address}>. Status code response: {result.status_code} - {status}"
)
return SuggestionSent(
success=False, status=f"Could not sent message: {status}"
)


def create_suggestion_service(
suggestion_mailjet_rest_api_key: str,
suggestion_mailjet_rest_api_secret: str,
suggestion_sender_name: str,
suggestion_sender_email: str,
suggestion_recipient_name: str,
suggestion_recipient_email: str,
suggestion_mailjet_custom_id: str,
) -> SuggestionServiceInterface:
return MailjetSuggestionService(
mailjet_client=Client(
auth=(suggestion_mailjet_rest_api_key, suggestion_mailjet_rest_api_secret),
version="v3.1",
),
suggestion_sender_name=suggestion_sender_name,
suggestion_sender_email=suggestion_sender_email,
suggestion_recipient_name=suggestion_recipient_name,
suggestion_recipient_email=suggestion_recipient_email,
suggestion_mailjet_custom_id=suggestion_mailjet_custom_id,
)
Loading