From 64a4482ce382f05d2aadc36008dab4e512249694 Mon Sep 17 00:00:00 2001 From: John Koestner Date: Mon, 21 Aug 2023 19:27:11 -0400 Subject: [PATCH] update mailer to work with images --- app.py | 19 ++++++++- folioflex/dashboard/layouts.py | 67 -------------------------------- folioflex/portfolio/portfolio.py | 2 + folioflex/utils/cq.py | 4 +- folioflex/utils/mailer.py | 33 +++++++++++----- folioflex/version.py | 2 +- 6 files changed, 47 insertions(+), 80 deletions(-) diff --git a/app.py b/app.py index cf3ff77..e59fa10 100644 --- a/app.py +++ b/app.py @@ -21,6 +21,7 @@ from dash import dcc from dash import html from dash.dependencies import Input, Output, State +from dash.dash_table.Format import Format, Scheme from folioflex.dashboard import dashboard_helper, layouts from folioflex.dashboard.pages import ( @@ -839,7 +840,23 @@ def update_ManagerTable(manager_status, cq_pm): if manager_status == "ready": cq_pm = pd.read_json(cq_pm).reset_index() cq_pm["lookback_date"] = pd.to_datetime(cq_pm["lookback_date"], unit="ms") - manager_table = layouts.manager_fmt, cq_pm.to_dict("records") + # formatting floats + # TODO: put this in dashboard_helper function + manager_table = [ + { + "name": i, + "id": i, + **( + { + "type": "numeric", + "format": Format(precision=2, scheme=Scheme.fixed).group(True), + } + if cq_pm[i].dtype == "float64" + else {} + ), + } + for i in cq_pm.columns + ], cq_pm.to_dict("records") else: manager_table = (None, None) diff --git a/folioflex/dashboard/layouts.py b/folioflex/dashboard/layouts.py index c8d9d5f..da26d7a 100644 --- a/folioflex/dashboard/layouts.py +++ b/folioflex/dashboard/layouts.py @@ -244,73 +244,6 @@ ), ] -manager_fmt = [ - dict(id="index", name="index"), - dict(id="date", name="date"), - dict(id="lookback_date", name="lookback_date"), - dict( - id="cumulative_cost", - name="cumulative_cost", - type="numeric", - format=Format(precision=0, scheme=Scheme.fixed).group(True), - ), - dict( - id="market_value", - name="market_value", - type="numeric", - format=Format(precision=0, scheme=Scheme.fixed).group(True), - ), - dict( - id="return", - name="return", - type="numeric", - format=Format(precision=0, scheme=Scheme.fixed).group(True), - ), - dict( - id="dwrr_pct", - name="dwrr_pct", - type="numeric", - format=Format(precision=2, scheme=Scheme.percentage), - ), - dict( - id="dwrr_ann_pct", - name="dwrr_ann_pct", - type="numeric", - format=Format(precision=2, scheme=Scheme.percentage), - ), - dict( - id="realized", - name="realized", - type="numeric", - format=Format(precision=0, scheme=Scheme.fixed).group(True), - ), - dict( - id="unrealized", - name="unrealized", - type="numeric", - format=Format(precision=0, scheme=Scheme.fixed).group(True), - ), - dict( - id="cash", - name="cash", - type="numeric", - format=Format(precision=0, scheme=Scheme.fixed).group(True), - ), - dict( - id="equity", - name="equity", - type="numeric", - format=Format(precision=0, scheme=Scheme.fixed).group(True), - ), - dict(id="benchmark", name="benchmark"), - dict( - id="benchmark_dwrr_pct", - name="benchmark_dwrr_pct", - type="numeric", - format=Format(precision=2, scheme=Scheme.percentage), - ), -] - performance_fmt = [ dict(id="ticker", name="ticker"), dict(id="date", name="date"), diff --git a/folioflex/portfolio/portfolio.py b/folioflex/portfolio/portfolio.py index 97f27f7..7cb5792 100644 --- a/folioflex/portfolio/portfolio.py +++ b/folioflex/portfolio/portfolio.py @@ -1334,6 +1334,8 @@ def get_summary(self, date=None, lookbacks=None): """ if lookbacks is None: lookbacks = [None] + elif isinstance(lookbacks, int): + lookbacks = [lookbacks] portfolio_repr = ", ".join([portfolio.name for portfolio in self.portfolios]) logger.info( f"Summarizing following portfolios: [{portfolio_repr}] with lookbacks {lookbacks}" diff --git a/folioflex/utils/cq.py b/folioflex/utils/cq.py index bb1338f..3a3281a 100644 --- a/folioflex/utils/cq.py +++ b/folioflex/utils/cq.py @@ -105,7 +105,7 @@ def portfolio_query(config_file, broker="all", lookback=None): @celery_app.task -def manager_query(config_file, lookback=None): +def manager_query(config_file, lookbacks=None): """Query for worker to generate manager. Parameters @@ -123,6 +123,6 @@ def manager_query(config_file, lookback=None): config_path = config_helper.CONFIG_PATH / config_file pm = portfolio.Manager(config_path=config_path) - cq_pm = pm.get_summary(lookback=lookback).to_json() + cq_pm = pm.get_summary(lookbacks=lookbacks).to_json() return cq_pm diff --git a/folioflex/utils/mailer.py b/folioflex/utils/mailer.py index dab0824..8207ad0 100644 --- a/folioflex/utils/mailer.py +++ b/folioflex/utils/mailer.py @@ -1,10 +1,16 @@ -"""Emailer.""" +""" +Email module. + +This module contains functions to send emails as well as generate reports +to send in the emails. + +""" -import base64 import datetime import logging import smtplib +from email.mime.image import MIMEImage from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText @@ -29,7 +35,7 @@ logger.addHandler(console_handler) -def send_email(message, subject, email_list): +def send_email(message, subject, email_list, image=None): """Send summary of portfolios to email. Parameters @@ -40,6 +46,8 @@ def send_email(message, subject, email_list): Subject of email email_list : list Email addresses to send email to + image : bytes (optional) (default=None) + Image to attach to email Returns ---------- @@ -62,6 +70,14 @@ def send_email(message, subject, email_list): message = MIMEText(message, "html") email.attach(message) + if image is not None: + email_image = MIMEImage(image) + email_image.add_header("Content-ID", "image1") + email_image.add_header( + "Content-Disposition", "attachment", filename="attach.png" + ) + email.attach(email_image) + # Send the email try: with smtplib.SMTP(config_helper.SMTP_SERVER, config_helper.SMTP_PORT) as smtp: @@ -115,20 +131,19 @@ def generate_report( today = datetime.date.today() subject = f"Summary as of {today}" message = "Below is your financial summary.

" + image = None if heatmap_dict is not None: portfolio = heatmap_dict.get("portfolio", None) lookback = heatmap_dict.get("lookback", None) heatmap_summary = heatmap.get_heatmap(portfolio=portfolio, lookback=lookback) - # using plotly kaleido to convert to image into bytes then base64 encode as - # this is standard practice for emails. - heatmap_bytes = heatmap_summary.to_image(format="png") - heatmap_img = base64.b64encode(heatmap_bytes).decode("utf-8") + # using plotly kaleido to convert to image into bytes then attach it to the email. + image = heatmap_summary.to_image(format="png") message += ( f"

Here is the heatmap as of {today}:

" - f"" + "
" + f"heatmap" + "
" ) if manager_dict is not None: @@ -169,4 +184,4 @@ def generate_report( + "
" ) - return send_email(message, subject=subject, email_list=email_list) + return send_email(message, subject=subject, email_list=email_list, image=image) diff --git a/folioflex/version.py b/folioflex/version.py index 45dc5ad..9f7a875 100644 --- a/folioflex/version.py +++ b/folioflex/version.py @@ -1 +1 @@ -version = "0.4.1" +version = "0.1.0"