Skip to content

Commit 7277831

Browse files
Stable release 1.2.0
Added fill_value argument on aggregate function; this value also is callable without arguments. Added send method on Report class; with this method you send report via email. Added send method on ReportBook class; with this method you send report via email. Fix __str__ method on Report class.
2 parents 8abe293 + 14b005d commit 7277831

File tree

5 files changed

+98
-5
lines changed

5 files changed

+98
-5
lines changed

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Release notes
22

3+
## 1.2.0
4+
Aug 5, 2021
5+
6+
- Added _fill_value_ argument on **aggregate** function; this value also is callable without arguments
7+
- Added _send_ method on **Report** class; with this method you send report via email
8+
- Added _send_ method on **ReportBook** class; with this method you send report via email
9+
- Fix \*__str__* method on **Report** class
10+
311
## 1.1.0
412
Jun 5, 2021
513

__info__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
"""Information variable used by modules on this package."""
2424

25-
__version__ = '1.1.0'
25+
__version__ = '1.2.0'
2626
__author__ = 'Matteo Guadrini'
2727
__email__ = '[email protected]'
2828
__homepage__ = 'https://github.com/MatteoGuadrini/pyreports'

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ <h2 class="title">Features</h2>
151151
<li><i class="fas fa-check"></i> All objects are extensible</li>
152152
<li><i class="fas fa-check"></i> Functions that support data modification</li>
153153
<li><i class="fas fa-check"></i> Data exports to Excel, CSV, YAML, Json and more.</li>
154+
<li><i class="fas fa-check"></i> Send exported data to email</li>
154155
</ul>
155156
</div><!--//container-->
156157
</section><!--//features-->

pyreports/core.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424

2525
# region Imports
2626
import tablib
27+
import smtplib
28+
import ssl
29+
from email.mime.text import MIMEText
30+
from email.mime.multipart import MIMEMultipart
31+
from email import encoders
32+
from email.mime.base import MIMEBase
2733
from .io import FileManager
2834
from .exception import ReportManagerError, ReportDataError
2935

@@ -245,7 +251,7 @@ def __str__(self):
245251
246252
:return: string
247253
"""
248-
return self._print_data()
254+
return str(self._print_data())
249255

250256
def _print_data(self):
251257
"""Print data and count
@@ -295,6 +301,64 @@ def export(self):
295301
else:
296302
raise ReportManagerError('the output object must be FileManager or NoneType object')
297303

304+
def send(self, server, _from, to, cc=None, bcc=None, subject=None, body='', auth=None, _ssl=True, headers=None):
305+
"""Send saved report to email
306+
307+
:param server: server SMTP
308+
:param _from: email address 'from:'
309+
:param to: email address 'to:'
310+
:param cc: email address 'cc:'
311+
:param bcc: email address 'bcc:'
312+
:param subject: email subject. Default is report title
313+
:param body: email body
314+
:param auth: authorization tuple "(user, password)"
315+
:param _ssl: boolean, if True port is 465 else 25
316+
:param headers: more header value "(header_name, key, value)"
317+
:return: None
318+
"""
319+
if not self.output:
320+
raise ReportDataError('if you want send a mail with a report in attachment, must be specified output')
321+
322+
# Prepare mail header
323+
message = MIMEMultipart("alternative")
324+
message["Subject"] = self.title if not subject else subject
325+
message["From"] = _from
326+
message["To"] = to
327+
if cc:
328+
message["Cc"] = cc
329+
if bcc:
330+
message["Bcc"] = bcc
331+
if headers:
332+
message.add_header(*headers)
333+
334+
# Prepare body
335+
part = MIMEText(body, "html")
336+
message.attach(part)
337+
338+
# Prepare attachment
339+
self.export()
340+
attach_file_name = self.output.file
341+
attach_file = open(attach_file_name, 'rb')
342+
payload = MIMEBase('application', 'octate-stream')
343+
payload.set_payload(attach_file.read())
344+
encoders.encode_base64(payload)
345+
payload.add_header('Content-Disposition', 'attachment', filename=attach_file_name)
346+
message.attach(payload)
347+
348+
# Prepare SMTP connection
349+
if _ssl:
350+
port = smtplib.SMTP_SSL_PORT
351+
protocol = smtplib.SMTP_SSL
352+
kwargs = {'context': ssl.create_default_context()}
353+
else:
354+
port = smtplib.SMTP_PORT
355+
protocol = smtplib.SMTP
356+
kwargs = {}
357+
with protocol(server, port, **kwargs) as srv:
358+
if auth:
359+
srv.login(*auth)
360+
srv.sendmail(_from, to, message.as_string())
361+
298362

299363
class ReportBook:
300364

@@ -395,4 +459,23 @@ def export(self, output=None):
395459
for report in self:
396460
report.export()
397461

462+
def send(self, server, _from, to, cc=None, bcc=None, subject=None, body='', auth=None, _ssl=True, headers=None):
463+
"""Send saved report to email
464+
465+
:param server: server SMTP
466+
:param _from: email address 'from:'
467+
:param to: email address 'to:'
468+
:param cc: email address 'cc:'
469+
:param bcc: email address 'bcc:'
470+
:param subject: email subject. Default is report title
471+
:param body: email body
472+
:param auth: authorization tuple "(user, password)"
473+
:param _ssl: boolean, if True port is 465 else 25
474+
:param headers: more header value "(header_name, key, value)"
475+
:return: None
476+
"""
477+
for report in self:
478+
report.send(server, _from, to, cc=cc, bcc=bcc, subject=subject, body=body, auth=auth,
479+
_ssl=_ssl, headers=headers)
480+
398481
# endregion

pyreports/datatools.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,13 @@ def counter(data, column):
109109
return Counter((item for item in data))
110110

111111

112-
def aggregate(*columns, fill_empty: bool = False):
112+
def aggregate(*columns, fill_empty: bool = False, fill_value=None):
113113
"""
114114
Aggregate in a new Dataset the columns
115115
116116
:param columns: columns added
117-
:param fill_empty: fill the empty field of data with None
117+
:param fill_empty: fill the empty field of data with "fill_value" argument
118+
:param fill_value: fill value for empty field if "fill_empty" argument is specified
118119
:return: Dataset
119120
"""
120121
if len(columns) >= 2:
@@ -124,7 +125,7 @@ def aggregate(*columns, fill_empty: bool = False):
124125
for list_ in columns[1:]:
125126
if fill_empty:
126127
while len(last_list) != len(list_):
127-
list_.append(None)
128+
list_.append(fill_value() if callable(fill_value) else fill_value)
128129
else:
129130
if len(last_list) != len(list_):
130131
raise InvalidDimensions('the columns are not the same length')

0 commit comments

Comments
 (0)