diff --git a/README.md b/README.md index 5f71261..58f6a03 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This is a template for building APIs with Clean Architecture. It contains two ex ## Prepare ```bash +cp envExample .env make build network ``` @@ -25,20 +26,27 @@ make build network make api.fastapi.run ``` -Now, you can test the API with the following command: - -```bash -curl -X 'GET' 'http://localhost:8000/api/v1/documents/?appName=app_test&clientId=client_test' -H 'accept: application/json' | jq -``` - ## Run Flask ```bash make api.flask.run ``` +## API -Now, you can test the API with the following command: +### Welcome ```bash -curl -X 'GET' 'http://localhost:5000/api/v1/documents/?appName=app_test&clientId=client_test' -H 'accept: application/json' | jq +curl -X 'GET' 'http://localhost:5000' -H 'accept: application/json' +``` + +### Documents + +```bash +curl -X 'GET' 'http://localhost:5000/api/v1/documents/?appName=app_test&clientId=client_test' -H 'accept: application/json' +``` + +## SEE + +```bash +curl -N -H "Accept:text/event-stream" http://localhost:5000/sse/alerts/ ``` diff --git a/envExample b/envExample new file mode 100644 index 0000000..e69de29 diff --git a/src/core/use_case/alerts_use_case.py b/src/core/use_case/alerts_use_case.py new file mode 100644 index 0000000..235ae19 --- /dev/null +++ b/src/core/use_case/alerts_use_case.py @@ -0,0 +1,37 @@ +from pydantic import BaseModel, StrictStr, StrictInt + +from src.core.decorators import check_params +from faker import Faker +from src.core.entity.ResponseTypes import ResponseTypes +import random + + +class GetNextAlertModel(BaseModel): + type_alert: StrictStr + point: StrictInt + +faker = Faker() + +@check_params(GetNextAlertModel) +def get_next_alert(params) -> dict: + """ + Get next alert + + :param GetNextAlertModel params: + :return: dict + """ + return { + "type": ResponseTypes.SUCCESS, + "error": None, + "data": { + "id": faker.uuid4(), + "type": random.choice(["ok", "warning", "danger"]), + "message": random.choice([ + f"The node number {random.randint(1, 8)} needs attention", + f"The connection with the node number {random.randint(1, 8)} is unstable", + f"The node number {random.randint(1, 8)} is offline", + f"The node number {random.randint(1, 8)} is overheating", + f"The node number {random.randint(1, 8)} is under maintenance", + ]), + } + } diff --git a/src/infra/api/flask/Dockerfile b/src/infra/api/flask/Dockerfile index ace4485..bb14d70 100644 --- a/src/infra/api/flask/Dockerfile +++ b/src/infra/api/flask/Dockerfile @@ -2,3 +2,4 @@ FROM base-core-app # launcher ENTRYPOINT flask --app main run --host=0.0.0.0 --debug +#ENTRYPOINT gunicorn main:app --worker-class gevent --bind 0.0.0.0:5000 diff --git a/src/infra/api/flask/main.py b/src/infra/api/flask/main.py index 9eceeab..e0647e7 100644 --- a/src/infra/api/flask/main.py +++ b/src/infra/api/flask/main.py @@ -1,21 +1,20 @@ import os +import time +import random -from flask import Flask, request +from flask import Flask, request, Response -from src.core.use_case import documents_use_case -from src.infra.storage.AzureStorage import AzureStorage from src.infra.api.flask.middlewares import register_middlewares +from src.infra.api.flask.routes.sse_routes import sse_routes +from src.infra.api.flask.routes.api_documents_routes import documents_routes app = Flask(__name__) - register_middlewares(app) +# Blueprints +app.register_blueprint(documents_routes) +app.register_blueprint(sse_routes) -@app.route("/api/v1/documents/") -def documents_list(): - storage = AzureStorage( - account_name=os.getenv("AZURE_STORAGE_ACCOUNT_NAME", ""), - account_key=os.getenv("AZURE_STORAGE_ACCOUNT_KEY", ""), - ) - params = request.args - return documents_use_case.documents_list(storage=storage, params=params) +@app.route('/') +def index(): + return {'Welcome to Clean Architecture template!': 'The endpoint you are looking for is not in this castle. Read the documentation to find the right path.'} diff --git a/src/infra/api/flask/middlewares.py b/src/infra/api/flask/middlewares.py index c081120..0964ca2 100644 --- a/src/infra/api/flask/middlewares.py +++ b/src/infra/api/flask/middlewares.py @@ -69,9 +69,10 @@ def register_middlewares(app): """ Convert the keys of the response to camel case (snake_case to camelCase) in all levels """ - response.data = json.dumps( - convert_keys_to_camel_case(json.loads(response.data)) - ) + if response.status_code == 200 and request.headers.get("Accept", None) != "text/event-stream": + response.data = json.dumps( + convert_keys_to_camel_case(json.loads(response.data)) + ) return response @app.after_request @@ -79,7 +80,8 @@ def register_middlewares(app): """ Convert the response to JSON format """ - response.headers["Content-Type"] = "application/json" + if response.status_code == 200 and request.headers.get("Accept", None) != "text/event-stream": + response.headers["Content-Type"] = "application/json" return response @app.after_request diff --git a/src/infra/api/flask/requirements.txt b/src/infra/api/flask/requirements.txt index 7e10602..f51d321 100644 --- a/src/infra/api/flask/requirements.txt +++ b/src/infra/api/flask/requirements.txt @@ -1 +1,3 @@ flask +gunicorn +gevent diff --git a/src/infra/api/flask/routes/api_documents_routes.py b/src/infra/api/flask/routes/api_documents_routes.py new file mode 100644 index 0000000..46ef821 --- /dev/null +++ b/src/infra/api/flask/routes/api_documents_routes.py @@ -0,0 +1,16 @@ +import os +from flask import Blueprint, request +from src.core.use_case import documents_use_case +from src.infra.storage.AzureStorage import AzureStorage + +documents_routes = Blueprint("api_documents_routes", __name__, url_prefix="/api/v1/documents") + + +@documents_routes.route("/") +def documents_list(): + storage = AzureStorage( + account_name=os.getenv("AZURE_STORAGE_ACCOUNT_NAME", ""), + account_key=os.getenv("AZURE_STORAGE_ACCOUNT_KEY", ""), + ) + params = request.args + return documents_use_case.documents_list(storage=storage, params=params) diff --git a/src/infra/api/flask/routes/sse_routes.py b/src/infra/api/flask/routes/sse_routes.py new file mode 100644 index 0000000..819d090 --- /dev/null +++ b/src/infra/api/flask/routes/sse_routes.py @@ -0,0 +1,24 @@ +import time +import random +from flask import Blueprint, Response +from src.core.use_case import alerts_use_case + +sse_routes = Blueprint("sse_routes", __name__, url_prefix="/sse") + +@sse_routes.route("/alerts/", methods=["GET"]) +def sse_alerts(): + """ + SSE endpoint to stream alerts + """ + def generate(): + try: + while True: + new_data = alerts_use_case.get_next_alert(params={"type_alert": "test", "point": 1})['data'] + yield f"data: {new_data}\n\n" + time.sleep(random.randint(1, 5)) + except GeneratorExit: + print("SSE stream closed") + except Exception as e: + print(f"Error in SSE stream: {e}") + + return Response(generate(), content_type="text/event-stream", status=200)