import typing from celery.exceptions import TimeoutError from celery.result import AsyncResult from flask import url_for from flask_atlas_auth import current_user from core_notifications.analytics import emails_events from core_notifications.consts import SendingTaskStatus, SystemEvents from core_notifications.logs import logger from core_notifications.models import Template from core_notifications.settings import Settings from core_notifications.tasks import send_email_task from core_notifications.templating.services import RenderLiquidService def schedule_emails_batch(data: typing.Iterable) -> typing.List[typing.Dict]: """ Service function to schedule emails sending based on input data. :param data: iterable with emails batch data Expected data structure: [ { "to": "test1@example.com", "subject": "some title", "body": "Hello world!", "cc": "test_cc1@example.com", "campaign_id": "some analytics id", "track_clicks": True }, { "to": "test1@example.com", "subject": "some title", "body": "Hello world!", "cc": ["test_cc1@example.com", "test_cc1@example.com"], "campaign_id": "some analytics id", "track_clicks": True }, { "to": ["test2@example.com", "test3@example.com"], "template": {"id": 1, "context": {"var1": "val1"}}, "cc": ["test_cc1@example.com", "test_cc2@example.com"], "campaign_id": "some analytics id", "track_clicks": True }, ] :return: list of dicts with tasks ids The return result would contain emails and tasks ids: [ { "test1@example.com": "some_id1", }, { "test2@example.com": "some_id2", "test3@example.com": "some_id3", } ] """ templates = {} result = [] for item in data: template_data = item.get("template") template = None if template_data: template_id = template_data.get("id") context = template_data.get("context") try: template = templates.get(template_id) or Template.query.get( template_id ) subject = template.subject source = template.source except Exception as e: logger.bind(item=item, error=e).error("Invalid template data") continue templates[template_id] = template item["subject"] = RenderLiquidService.execute(subject, context) item["body"] = RenderLiquidService.execute(source, context) del item["template"] item["analytics"] = { "subject": item["subject"], "user_id": current_user.id, "user_name": current_user.name, } if template: item["analytics"].update( {"template_id": template.id, "template_name": template.name} ) campaign_id = item.pop("campaign_id", None) if campaign_id: item["analytics"]["campaign_id"] = campaign_id item["analytic_url"] = url_for( "analytics.link_click", _external=True, ) logger.bind( to=item["to"], subject=item["subject"], cc=item.get("cc") ).info("Scheduling email") if isinstance(item["to"], list): result_item = {} for to in item["to"]: kwargs = {**item, "to": to} task_id = send_email_task.apply_async(kwargs=kwargs).task_id result_item[to] = task_id emails_events.log( SystemEvents.sending_email, id=task_id, to=to, **item["analytics"], ) result.append(result_item) else: task_id = send_email_task.apply_async(kwargs=item).task_id result.append({item["to"]: task_id}) emails_events.log( SystemEvents.sending_email, id=task_id, to=item["to"], **item["analytics"], ) return result def get_sending_status(task_id: str) -> typing.Optional[SendingTaskStatus]: """ :param task_id: id of scheduled sending task :return: core_notifications.consts.SendingTaskStatus """ async_result = AsyncResult(task_id) celery_state = async_result.state # we need this extra check to ensure that the task is really exists # because AsyncResult().state will always be PENDING # even if there is no any task with this ID try: async_result.get(timeout=Settings.TASK_GET_RESULT_TIMEOUT) except TimeoutError: celery_state = None logger.bind(task_id=task_id).warning("No task with ID") return SendingTaskStatus.from_celery_state(celery_state)