Skip to content

Commit 6ae7967

Browse files
committed
move notification header to global context
1 parent dd1c7d2 commit 6ae7967

File tree

14 files changed

+279
-237
lines changed

14 files changed

+279
-237
lines changed

superset/reports/commands/execute.py

+4-30
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,14 @@
6363
ReportRecipientType,
6464
ReportSchedule,
6565
ReportScheduleType,
66-
ReportSourceFormat,
6766
ReportState,
6867
)
6968
from superset.reports.notifications import create_notification
7069
from superset.reports.notifications.base import NotificationContent
7170
from superset.reports.notifications.exceptions import NotificationError
7271
from superset.tasks.utils import get_executor
7372
from superset.utils.celery import session_scope
74-
from superset.utils.core import HeaderDataType, override_user
73+
from superset.utils.core import override_user
7574
from superset.utils.csv import get_chart_csv_data, get_chart_dataframe
7675
from superset.utils.decorators import context
7776
from superset.utils.screenshots import ChartScreenshot, DashboardScreenshot
@@ -351,27 +350,6 @@ def _update_query_context(self) -> None:
351350
"Please try loading the chart and saving it again."
352351
) from ex
353352

354-
def _get_log_data(self) -> HeaderDataType:
355-
chart_id = None
356-
dashboard_id = None
357-
report_source = None
358-
if self._report_schedule.chart:
359-
report_source = ReportSourceFormat.CHART
360-
chart_id = self._report_schedule.chart_id
361-
else:
362-
report_source = ReportSourceFormat.DASHBOARD
363-
dashboard_id = self._report_schedule.dashboard_id
364-
365-
log_data: HeaderDataType = {
366-
"notification_type": self._report_schedule.type,
367-
"notification_source": report_source,
368-
"notification_format": self._report_schedule.report_format,
369-
"chart_id": chart_id,
370-
"dashboard_id": dashboard_id,
371-
"owners": self._report_schedule.owners,
372-
}
373-
return log_data
374-
375353
def _get_notification_content(self) -> NotificationContent:
376354
"""
377355
Gets a notification content, this is composed by a title and a screenshot
@@ -382,7 +360,6 @@ def _get_notification_content(self) -> NotificationContent:
382360
embedded_data = None
383361
error_text = None
384362
screenshot_data = []
385-
header_data = self._get_log_data()
386363
url = self._get_url(user_friendly=True)
387364
if (
388365
feature_flag_manager.is_feature_enabled("ALERTS_ATTACH_REPORTS")
@@ -403,7 +380,6 @@ def _get_notification_content(self) -> NotificationContent:
403380
return NotificationContent(
404381
name=self._report_schedule.name,
405382
text=error_text,
406-
header_data=header_data,
407383
)
408384

409385
if (
@@ -430,7 +406,6 @@ def _get_notification_content(self) -> NotificationContent:
430406
description=self._report_schedule.description,
431407
csv=csv_data,
432408
embedded_data=embedded_data,
433-
header_data=header_data,
434409
)
435410

436411
def _send(
@@ -498,16 +473,15 @@ def send_error(self, name: str, message: str) -> None:
498473
499474
:raises: CommandException
500475
"""
501-
header_data = self._get_log_data()
502476
logger.info(
503-
"header_data in notifications for alerts and reports %s, taskid, %s",
504-
header_data,
477+
"An error for a notification occurred, sending error notification",
505478
extra={
506479
"execution_id": self._execution_id,
507480
},
508481
)
509482
notification_content = NotificationContent(
510-
name=name, text=message, header_data=header_data
483+
name=name,
484+
text=message,
511485
)
512486

513487
# filter recipients to recipients who are also owners

superset/reports/notifications/base.py

-2
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@
2020
import pandas as pd
2121

2222
from superset.reports.models import ReportRecipients, ReportRecipientType
23-
from superset.utils.core import HeaderDataType
2423

2524

2625
@dataclass
2726
class NotificationContent:
2827
name: str
29-
header_data: HeaderDataType # this is optional to account for error states
3028
csv: Optional[bytes] = None # bytes for csv file
3129
screenshots: Optional[list[bytes]] = None # bytes for a list of screenshots
3230
text: Optional[str] = None

superset/reports/notifications/email.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from superset.reports.models import ReportRecipientType
3131
from superset.reports.notifications.base import BaseNotification
3232
from superset.reports.notifications.exceptions import NotificationError
33-
from superset.utils.core import HeaderDataType, send_email_smtp
33+
from superset.reports.notifications.utils import send_email_smtp
3434
from superset.utils.decorators import statsd_gauge
3535

3636
logger = logging.getLogger(__name__)
@@ -68,7 +68,6 @@
6868
@dataclass
6969
class EmailContent:
7070
body: str
71-
header_data: Optional[HeaderDataType] = None
7271
data: Optional[dict[str, Any]] = None
7372
images: Optional[dict[str, bytes]] = None
7473

@@ -173,7 +172,6 @@ def _get_content(self) -> EmailContent:
173172
body=body,
174173
images=images,
175174
data=csv_data,
176-
header_data=self._content.header_data,
177175
)
178176

179177
def _get_subject(self) -> str:
@@ -204,13 +202,13 @@ def send(self) -> None:
204202
bcc="",
205203
mime_subtype="related",
206204
dryrun=False,
207-
header_data=content.header_data,
208205
)
209206
logger.info(
210-
"Report sent to email, notification content is %s",
211-
content.header_data,
207+
"Report sent to email",
212208
extra={
213209
"execution_id": global_context.get("execution_id"),
210+
"dashboard_id": global_context.get("dashboard_id"),
211+
"chart_id": global_context.get("chart_id"),
214212
},
215213
)
216214
except SupersetErrorsException as ex:
+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
19+
import logging
20+
import os
21+
import re
22+
import smtplib
23+
import ssl
24+
from email.mime.application import MIMEApplication
25+
from email.mime.image import MIMEImage
26+
from email.mime.multipart import MIMEMultipart
27+
from email.mime.text import MIMEText
28+
from email.utils import formatdate
29+
from typing import Any, Optional
30+
31+
from flask import g
32+
33+
from superset import db
34+
from superset.reports.models import ReportSchedule, ReportSourceFormat
35+
from superset.reports.types import HeaderDataType
36+
37+
logger = logging.getLogger(__name__)
38+
39+
40+
def get_email_address_list(address_string: str) -> list[str]:
41+
address_string_list: list[str] = []
42+
if isinstance(address_string, str):
43+
address_string_list = re.split(r",|\s|;", address_string)
44+
return [x.strip() for x in address_string_list if x.strip()]
45+
46+
47+
def _get_log_data() -> HeaderDataType:
48+
global_context = getattr(g, "context", {}) or {}
49+
chart_id = global_context.get("chart_id")
50+
dashboard_id = global_context.get("dashboard_id")
51+
report_schedule_id = global_context.get("report_schedule_id")
52+
report_source: str = ""
53+
report_format: str = ""
54+
report_type: str = ""
55+
owners: list[int] = []
56+
57+
# intentionally creating a new session to
58+
# keep the logging session separate from the
59+
# main session
60+
session = db.create_scoped_session()
61+
report_schedule = (
62+
session.query(ReportSchedule).filter_by(id=report_schedule_id).one_or_none()
63+
)
64+
session.close()
65+
66+
if report_schedule is not None:
67+
report_type = report_schedule.type
68+
report_format = report_schedule.report_format
69+
owners = report_schedule.owners
70+
report_source = (
71+
ReportSourceFormat.DASHBOARD
72+
if report_schedule.dashboard_id
73+
else ReportSourceFormat.CHART
74+
)
75+
76+
log_data: HeaderDataType = {
77+
"notification_type": report_type,
78+
"notification_source": report_source,
79+
"notification_format": report_format,
80+
"chart_id": chart_id,
81+
"dashboard_id": dashboard_id,
82+
"owners": owners,
83+
}
84+
return log_data
85+
86+
87+
def send_email_smtp(
88+
to: str,
89+
subject: str,
90+
html_content: str,
91+
config: dict[str, Any],
92+
files: Optional[list[str]] = None,
93+
data: Optional[dict[str, str]] = None,
94+
images: Optional[dict[str, bytes]] = None,
95+
dryrun: bool = False,
96+
cc: Optional[str] = None,
97+
bcc: Optional[str] = None,
98+
mime_subtype: str = "mixed",
99+
) -> None:
100+
"""
101+
Send an email with html content, eg:
102+
send_email_smtp(
103+
'[email protected]', 'foo', '<b>Foo</b> bar',['/dev/null'], dryrun=True)
104+
"""
105+
smtp_mail_from = config["SMTP_MAIL_FROM"]
106+
smtp_mail_to = get_email_address_list(to)
107+
108+
msg = MIMEMultipart(mime_subtype)
109+
msg["Subject"] = subject
110+
msg["From"] = smtp_mail_from
111+
msg["To"] = ", ".join(smtp_mail_to)
112+
113+
msg.preamble = "This is a multi-part message in MIME format."
114+
115+
recipients = smtp_mail_to
116+
if cc:
117+
smtp_mail_cc = get_email_address_list(cc)
118+
msg["CC"] = ", ".join(smtp_mail_cc)
119+
recipients = recipients + smtp_mail_cc
120+
121+
if bcc:
122+
# don't add bcc in header
123+
smtp_mail_bcc = get_email_address_list(bcc)
124+
recipients = recipients + smtp_mail_bcc
125+
126+
msg["Date"] = formatdate(localtime=True)
127+
mime_text = MIMEText(html_content, "html")
128+
msg.attach(mime_text)
129+
130+
# Attach files by reading them from disk
131+
for fname in files or []:
132+
basename = os.path.basename(fname)
133+
with open(fname, "rb") as f:
134+
msg.attach(
135+
MIMEApplication(
136+
f.read(),
137+
Content_Disposition=f"attachment; filename='{basename}'",
138+
Name=basename,
139+
)
140+
)
141+
142+
# Attach any files passed directly
143+
for name, body in (data or {}).items():
144+
msg.attach(
145+
MIMEApplication(
146+
body, Content_Disposition=f"attachment; filename='{name}'", Name=name
147+
)
148+
)
149+
150+
# Attach any inline images, which may be required for display in
151+
# HTML content (inline)
152+
for msgid, imgdata in (images or {}).items():
153+
formatted_time = formatdate(localtime=True)
154+
file_name = f"{subject} {formatted_time}"
155+
image = MIMEImage(imgdata, name=file_name)
156+
image.add_header("Content-ID", f"<{msgid}>")
157+
image.add_header("Content-Disposition", "inline")
158+
msg.attach(image)
159+
msg_mutator = config["EMAIL_HEADER_MUTATOR"]
160+
# the base notification returns the message without any editing.
161+
header_data = _get_log_data()
162+
new_msg = msg_mutator(msg, **(header_data or {}))
163+
send_mime_email(smtp_mail_from, recipients, new_msg, config, dryrun=dryrun)
164+
165+
166+
def send_mime_email(
167+
e_from: str,
168+
e_to: list[str],
169+
mime_msg: MIMEMultipart,
170+
config: dict[str, Any],
171+
dryrun: bool = False,
172+
) -> None:
173+
smtp_host = config["SMTP_HOST"]
174+
smtp_port = config["SMTP_PORT"]
175+
smtp_user = config["SMTP_USER"]
176+
smtp_password = config["SMTP_PASSWORD"]
177+
smtp_starttls = config["SMTP_STARTTLS"]
178+
smtp_ssl = config["SMTP_SSL"]
179+
smtp_ssl_server_auth = config["SMTP_SSL_SERVER_AUTH"]
180+
181+
if dryrun:
182+
logger.info("Dryrun enabled, email notification content is below:")
183+
logger.info(mime_msg.as_string())
184+
return
185+
186+
# Default ssl context is SERVER_AUTH using the default system
187+
# root CA certificates
188+
ssl_context = ssl.create_default_context() if smtp_ssl_server_auth else None
189+
smtp = (
190+
smtplib.SMTP_SSL(smtp_host, smtp_port, context=ssl_context)
191+
if smtp_ssl
192+
else smtplib.SMTP(smtp_host, smtp_port)
193+
)
194+
if smtp_starttls:
195+
smtp.starttls(context=ssl_context)
196+
if smtp_user and smtp_password:
197+
smtp.login(smtp_user, smtp_password)
198+
logger.debug("Sent an email to %s", str(e_to))
199+
smtp.sendmail(e_from, e_to, mime_msg.as_string())
200+
smtp.quit()

superset/reports/types.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,19 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17-
from typing import TypedDict
17+
from typing import Optional, TypedDict
1818

1919
from superset.dashboards.permalink.types import DashboardPermalinkState
2020

2121

2222
class ReportScheduleExtra(TypedDict):
2323
dashboard: DashboardPermalinkState
24+
25+
26+
class HeaderDataType(TypedDict):
27+
notification_format: str
28+
owners: list[int]
29+
notification_type: str
30+
notification_source: Optional[str]
31+
chart_id: Optional[int]
32+
dashboard_id: Optional[int]

0 commit comments

Comments
 (0)