Skip to content
Merged

Dev #54

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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ los mismos son:

* `POST` **/push** (Envio de notificaciones push a los usuarios por uuid)
* `GET` **/email?by=(id or email)** (Envio de correos a los usuarios por uuid o email. El parametro send_to_all es para admins y requiere autentificación con el JWT token de admins)

* `GET` **/user/permissions** (Obtiene los permisos de un usuario según su JWT token)
* `PUT` **/user/permissions** (Actualiza los permisos de un usuario según su JWT token)
*

* `GET` **/user/history** (Obtiene el historial de notificaciones push de un usuario según su JWT token.)
* `PUT` **/user/history** (Marca una notificación del historial como leída)

* `GET` **/logs**
* `GET` **/logs**

## Estructura

Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ dependencies = [
"requests>=2.32.3",
"sendgrid>=6.11.0",
"types-requests>=2.32.0.20250328",
"httpx>=0.28.1",
"asgi-lifespan>=2.1.0",
]

[tool.setuptools]
Expand Down
6 changes: 3 additions & 3 deletions src/app/api/v1/endpoints/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from fastapi import APIRouter, Depends, Query, Request, status

from app.core.logging import setup_logger
from app.dependencies import get_email_service
from app.errors.errors import error_response
from app.middleware.jw_admin_auth import jw_admin_middleware
from app.models.email import EmailPayload
Expand Down Expand Up @@ -106,6 +107,7 @@
async def send_email_notification(
payload: EmailPayload,
request: Request,
email_service: EmailService = Depends(get_email_service),
by: str = Query("id", enum=["id", "email"]),
is_admin: bool = Depends(jw_admin_middleware),
):
Expand Down Expand Up @@ -156,10 +158,8 @@ async def send_email_notification(
instance=str(request.url.path),
)

service = EmailService()

try:
response = await service.send_email(
response = await email_service.send_email(
payload, fetchEmails=(by == "id"), send_to_all=payload.send_to_all
)
response.raise_for_status()
Expand Down
13 changes: 8 additions & 5 deletions src/app/api/v1/endpoints/push.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import httpx
from fastapi import APIRouter, Request, status
from fastapi import APIRouter, Depends, Request, status

from app.core.logging import setup_logger
from app.dependencies import get_push_service
from app.errors.errors import error_response
from app.models.push import PushPayload
from app.services.push_service import PushService
Expand Down Expand Up @@ -58,7 +59,11 @@
},
},
)
async def send_push_notification(payload: PushPayload, request: Request):
async def send_push_notification(
payload: PushPayload,
request: Request,
push_service: PushService = Depends(get_push_service),
):
"""
Sends a push notification to the users if they have permissions enabled
"""
Expand All @@ -75,10 +80,8 @@ async def send_push_notification(payload: PushPayload, request: Request):
instance=str(request.url.path),
)

service = PushService()

try:
response = await service.send_push(payload)
response = await push_service.send_push(payload)
if not response:
return {"detail": "users given do not have permissions"}
response.raise_for_status()
Expand Down
32 changes: 32 additions & 0 deletions src/app/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import httpx
from fastapi import Depends

from app.services.email_service import EmailService
from app.services.push_service import PushService

_shared_http_client: httpx.AsyncClient | None = None


def set_global_http_client(client: httpx.AsyncClient):
global _shared_http_client
_shared_http_client = client


def get_global_http_client() -> httpx.AsyncClient:
if _shared_http_client is None:
raise RuntimeError(
"httpx.AsyncClient not initialized. Check application lifespan."
)
return _shared_http_client


def get_email_service(
http_client: httpx.AsyncClient = Depends(get_global_http_client),
) -> EmailService:
return EmailService(http_client=http_client)


def get_push_service(
http_client: httpx.AsyncClient = Depends(get_global_http_client),
) -> PushService:
return PushService(http_client=http_client)
24 changes: 22 additions & 2 deletions src/app/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from contextlib import asynccontextmanager

import httpx
from fastapi import FastAPI
from fastapi.exceptions import HTTPException, RequestValidationError

Expand All @@ -8,6 +9,7 @@

# from app.db.firebase import init_firebase
from app.db.mongo import close_mongo_connection, connect_to_mongo
from app.dependencies import set_global_http_client
from app.errors.errors import (
bad_request_exception_handler,
generic_exception_handler,
Expand All @@ -19,12 +21,31 @@

logger = setup_logger(__name__)

_internal_http_client: httpx.AsyncClient | None = None


@asynccontextmanager
async def lifespan(app: FastAPI):
# Connect to MongoDB
await connect_to_mongo()
yield
logger.info("MongoDB connection established.")

# Initialize the httpx.AsyncClient once for the entire application
global _internal_http_client
_internal_http_client = httpx.AsyncClient(base_url="https://api.brevo.com/v3/")
set_global_http_client(_internal_http_client)
logger.info("httpx.AsyncClient initialized.")

yield # Application starts here

# Close MongoDB connection
await close_mongo_connection()
logger.info("MongoDB connection closed.")

# Close the httpx.AsyncClient
if _internal_http_client:
await _internal_http_client.aclose()
logger.info("httpx.AsyncClient closed.")


app = FastAPI(
Expand All @@ -35,7 +56,6 @@ async def lifespan(app: FastAPI):
lifespan=lifespan,
)


app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(Exception, generic_exception_handler)
app.add_exception_handler(HTTPException, bad_request_exception_handler)
Expand Down
34 changes: 29 additions & 5 deletions src/app/services/email_service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from typing import List

import httpx
Expand All @@ -14,12 +15,29 @@

logger = setup_logger(__name__)

# Lectura del archivo html global
try:
base_dir = os.path.dirname(os.path.abspath(__file__))
template_path = os.path.join(base_dir, "../../static/email_template.html")
template_path = os.path.abspath(template_path)

print(f"Full path: {template_path}")
with open(template_path, "r", encoding="utf-8") as f:
EMAIL_TEMPLATE_HTML = f.read()

except FileNotFoundError:
logger.error("email_template.html not found. Please check the path.")
EMAIL_TEMPLATE_HTML = """<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8"></head>
<body>{{ params.body }}</body></html>"""


class EmailService:
def __init__(self):
def __init__(self, http_client: httpx.AsyncClient):
self.brevo_api_url = "https://api.brevo.com/v3/smtp/email"
self.brevo_api_key = settings.BREVO_API_KEY
self.mail_from = settings.MAIL_FROM
self.email_template_html = EMAIL_TEMPLATE_HTML
self.http_client = http_client

async def send_email(
self, payload: EmailPayload, fetchEmails: bool, send_to_all: bool = False
Expand Down Expand Up @@ -55,17 +73,23 @@ async def send_email(
"Content-Type": "application/json",
}

html_content_final = self.email_template_html.replace(
"{{ params.subject }}", payload.subject
).replace("{{ params.body }}", payload.body)

logger.info(f"html final content {html_content_final}")

data = {
"sender": {"email": self.mail_from},
"to": [{"email": recipient} for recipient in emails],
"templateId": 1,
"subject": payload.subject,
"params": {"subject": payload.subject, "body": payload.body},
"htmlContent": html_content_final,
}

logger.info(f"Sending email to {len(emails)} recipients")
async with httpx.AsyncClient() as client:
response = await client.post(self.brevo_api_url, json=data, headers=headers)
response = await self.http_client.post(
self.brevo_api_url, json=data, headers=headers
)

if response.status_code == 201:
await log_event(
Expand Down
8 changes: 5 additions & 3 deletions src/app/services/push_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@


class PushService:
def __init__(self):
def __init__(self, http_client: httpx.AsyncClient):
self.onesignal_url = "https://api.onesignal.com/notifications"
self.onesignal_apikey = settings.ONESIGNAL_API_KEY
self.onesignal_appid = settings.ONESIGNAL_APP_ID
self.http_client = http_client

async def send_push(self, payload: PushPayload):
external_user_ids, not_sent = await get_users_with_permissions(
Expand Down Expand Up @@ -69,8 +70,9 @@ async def send_push(self, payload: PushPayload):
}

logger.info(f"Sending email to {len(users_str)} recipients")
async with httpx.AsyncClient() as client:
response = await client.post(self.onesignal_url, json=data, headers=headers)
response = await self.http_client.post(
self.onesignal_url, json=data, headers=headers
)

if response.status_code in [200, 201]:
await log_event(
Expand Down
Loading