Encode CSV file for Sendgrid's Email API - python

I am attempting to build a client reporting engine using R/Python, and Sendgrid's Email API. I can send emails, but the last thing that I need to do is attach a client's CSV report.
I have attempted a number of approaches, including base64 encoding the file and writing the string to disk from R to python, but not luck. That said, it seems like I am getting stuck on this error:
TypeError: Object of type 'bytes' is not JSON serializable
My code to get there is:
with open('raw/test-report.csv', 'rb') as fd:
b64data = base64.b64encode(fd.read())
attachment = Attachment()
attachment.content = b64data
attachment.filename = "your-lead-report.csv"
mail.add_attachment(attachment)
What is confusing is that if I simply replace b64data with the line
attachment.content = 'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gQ3JhcyBwdW12'
an email is sent with an attachment.
For reference, I have been using:
https://github.com/sendgrid/sendgrid-python
and
kitchen sink tutorial
and haven't had any issues until this final step in my project.
Any help will be greatly appreciated. It's worth noting that my strength is in R, but I usually can hack things together in python with the help of the internets.

You need to convert b64data to a regular string before assigning it to attachment.content. Sendgrid builds a JSON payload which it sends in the requests so it doesn't know how to serialize the value assigned to attachment.content which in this case is a bytestring.
str(b64data,'utf-8')
References:
https://github.com/sendgrid/sendgrid-python/blob/master/USAGE.md#post-mailsend

Here's how to attach an in memory CSV to a sendgrid email:
import base64
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import (
Mail, Attachment, FileContent, FileName,
FileType, Disposition)
from io import BytesIO
import pandas as pd
def send_email_with_csv(from_address, to_address, subject, content, dataframe, filename, filetype):
message = Mail(
from_email=from_address,
to_emails=to_address,
subject=subject,
html_content=content)
#%% Create buffered csv
buffer = BytesIO()
dataframe.to_csv(buffer);
buffer.seek(0)
data = buffer.read()
encoded = base64.b64encode(data).decode()
#%%
attachment = Attachment()
attachment.file_content = FileContent(encoded)
attachment.file_type = FileType(filetype)
attachment.file_name = FileName(filename)
attachment.disposition = Disposition('attachment')
message.attachment = attachment
try:
sendgrid_client = SendGridAPIClient('API_KEY')
response = sendgrid_client.send(message)
print(response.status_code)
print(response.body)
print(response.headers)
except Exception as e:
print(e.message)
df = pd.DataFrame(data={'col1': [1, 2], 'col2': [3, 4]})
send_email_with_csv(from_address='sender#sender.com',
to_address='example#recipient.com',
subject='Sending with Twilio SendGrid is Fun',
content='<strong>and easy to do anywhere, even with Python</strong>',
dataframe=df,
filename='spreadsheet.csv',
filetype='text/csv')

Related

how to send pandas dataframe as an email attachment in python using exchangelib

I need to email the pandas dataframe df as an attachment using exchangelib.
below is my code
import io
attachments=[]
x=io.BytesIO(df.to_records(index=False))
content=x.getvalue()
type(content) #bytes
send_email(account, 'Subject', 'test email', ['testemail.com'],
attachments=attachments)
when i use the above code i get the email with attachment,but it's in the byte format so it's unreadable.
This is because the type(content) is "bytes"
Below code works perfectly fine.
This is because type(content) is "class 'bytes'> "
but i am not allowed to save my dataframe locally as .csv file.
I want send the dataframe directly as an email attachment.
How can I convert my df in the "class 'bytes'> " format and email the attachment in csv ?
attachments = []
with open('test.csv', 'rb') as f:
content = f.read()
type(content) #<class 'bytes'>
attachments.append(('test.csv', content))
below is send_email i am using
from exchangelib import Configuration, Account, DELEGATE
from exchangelib import Message, Mailbox, FileAttachment
def send_email(account, subject, body, recipients, attachments=None):
"""
Send an email.
Parameters
----------
account : Account object
subject : str
body : str
recipients : list of str
Each str is and email adress
attachments : list of tuples or None
(filename, binary contents)
Examples
--------
"""
to_recipients = []
for recipient in recipients:
to_recipients.append(Mailbox(email_address=recipient))
# Create message
m = Message(account=account,
folder=account.sent,
subject=subject,
body=body,
to_recipients=to_recipients)
# attach files
for attachment_name, attachment_content in attachments or []:
file = FileAttachment(name=attachment_name, content=attachment_content)
m.attach(file)
m.send_and_save()
File contents are always bytes, no matter which file type you are talking about. Programs may hide that fact from you, but it's still true.
When you write "it's in the byte format so it's unreadable", I assume you mean that your email client does not help you to view the file in the way you expected. To help your email client do that, try setting the content_type attribute of FileAttachment to something that matches the file type of your attachment. See Response Content type as CSV for suggested values.

Decoding message headers and bodies in python

I'm trying to program a simple mail client in Python which can both send and read messages from inbox in gmail. The sending part works well, but the reading part keeps giving me a bad result.
Here is the python code:
import imaplib
import base64
email_user = '<gmail_address>'
email_pass = '<password>'
M = imaplib.IMAP4_SSL('imap.gmail.com', 993)
M.login(email_user, email_pass)
M.select('inbox')
typ, message_numbers = M.search(None, 'ALL') # change variable name, and use new name in for loop
typ, data = M.fetch(b'1', '(RFC822)')
data1 = base64.b64decode(data[0][1])
print('Message \n%s' %data1)
M.close()
M.logout()
and the result is:
Message
b'\r\xe9b\xbd\xea\xdeu:1\xcaf\xa2\x97M;\x82f\xa2\x95\xca&E\xe7\x1e\x8a\xf7\x9do-\xb4\xd3f\xb5\xef\xdd\x1a\xd7Nt\xd3M4\xc2+aH\xc4\xcf\x89\xdc\xb5\xea\xfe\x9c\xb2\x9d\xb8\xd3\xae\xbd\xf2\x98\xdd2\x89\xf5\xd4W\x9b\xdbM}\xd3nv\xd7\x9d<\xd3C\xd2Mt^q\xe8\xafy\xd6\xf2\xdbM6i\xd7\xfc\xdbgp\x8a\xd8R13\xe2w\x8d\xa6\xaf^\xbb\xefo{\xf3\n\xdb\xeb}y\xe3\xdf<\xdb}\xf9\xdfM\x8c\xa2}u\x15\xe6\xf6\xd3_t\xdb\x9d\xb5\xe7O4\xd0\xf4\x93\x01\x10\x92y\xa9b\xd5\xaa\xecj\xc8Z\xdb\x9e\xad\xd7\x9e=\xf3\xcd\xb7\xdf\x97/\x9e\x89\xdev\n(\x82W\x9c\xa2k'
I appreciate any modified code and hint. Also I'm new to python so please keep your descriptions as simple as possible...
The message you receive in data[0][1] is not base64.
You want to do something like
from email import message_from_bytes
...
msg = message_from_bytes(data[0][1])
and then manipulate msg.

python requests post file using multipart form parameters [duplicate]

I'm performing a simple task of uploading a file using Python requests library. I searched Stack Overflow and no one seemed to have the same problem, namely, that the file is not received by the server:
import requests
url='http://nesssi.cacr.caltech.edu/cgi-bin/getmulticonedb_release2.cgi/post'
files={'files': open('file.txt','rb')}
values={'upload_file' : 'file.txt' , 'DB':'photcat' , 'OUT':'csv' , 'SHORT':'short'}
r=requests.post(url,files=files,data=values)
I'm filling the value of 'upload_file' keyword with my filename, because if I leave it blank, it says
Error - You must select a file to upload!
And now I get
File file.txt of size bytes is uploaded successfully!
Query service results: There were 0 lines.
Which comes up only if the file is empty. So I'm stuck as to how to send my file successfully. I know that the file works because if I go to this website and manually fill in the form it returns a nice list of matched objects, which is what I'm after. I'd really appreciate all hints.
Some other threads related (but not answering my problem):
Send file using POST from a Python script
http://docs.python-requests.org/en/latest/user/quickstart/#response-content
Uploading files using requests and send extra data
http://docs.python-requests.org/en/latest/user/advanced/#body-content-workflow
If upload_file is meant to be the file, use:
files = {'upload_file': open('file.txt','rb')}
values = {'DB': 'photcat', 'OUT': 'csv', 'SHORT': 'short'}
r = requests.post(url, files=files, data=values)
and requests will send a multi-part form POST body with the upload_file field set to the contents of the file.txt file.
The filename will be included in the mime header for the specific field:
>>> import requests
>>> open('file.txt', 'wb') # create an empty demo file
<_io.BufferedWriter name='file.txt'>
>>> files = {'upload_file': open('file.txt', 'rb')}
>>> print(requests.Request('POST', 'http://example.com', files=files).prepare().body.decode('ascii'))
--c226ce13d09842658ffbd31e0563c6bd
Content-Disposition: form-data; name="upload_file"; filename="file.txt"
--c226ce13d09842658ffbd31e0563c6bd--
Note the filename="file.txt" parameter.
You can use a tuple for the files mapping value, with between 2 and 4 elements, if you need more control. The first element is the filename, followed by the contents, and an optional content-type header value and an optional mapping of additional headers:
files = {'upload_file': ('foobar.txt', open('file.txt','rb'), 'text/x-spam')}
This sets an alternative filename and content type, leaving out the optional headers.
If you are meaning the whole POST body to be taken from a file (with no other fields specified), then don't use the files parameter, just post the file directly as data. You then may want to set a Content-Type header too, as none will be set otherwise. See Python requests - POST data from a file.
(2018) the new python requests library has simplified this process, we can use the 'files' variable to signal that we want to upload a multipart-encoded file
url = 'http://httpbin.org/post'
files = {'file': open('report.xls', 'rb')}
r = requests.post(url, files=files)
r.text
Client Upload
If you want to upload a single file with Python requests library, then requests lib supports streaming uploads, which allow you to send large files or streams without reading into memory.
with open('massive-body', 'rb') as f:
requests.post('http://some.url/streamed', data=f)
Server Side
Then store the file on the server.py side such that save the stream into file without loading into the memory. Following is an example with using Flask file uploads.
#app.route("/upload", methods=['POST'])
def upload_file():
from werkzeug.datastructures import FileStorage
FileStorage(request.stream).save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return 'OK', 200
Or use werkzeug Form Data Parsing as mentioned in a fix for the issue of "large file uploads eating up memory" in order to avoid using memory inefficiently on large files upload (s.t. 22 GiB file in ~60 seconds. Memory usage is constant at about 13 MiB.).
#app.route("/upload", methods=['POST'])
def upload_file():
def custom_stream_factory(total_content_length, filename, content_type, content_length=None):
import tempfile
tmpfile = tempfile.NamedTemporaryFile('wb+', prefix='flaskapp', suffix='.nc')
app.logger.info("start receiving file ... filename => " + str(tmpfile.name))
return tmpfile
import werkzeug, flask
stream, form, files = werkzeug.formparser.parse_form_data(flask.request.environ, stream_factory=custom_stream_factory)
for fil in files.values():
app.logger.info(" ".join(["saved form name", fil.name, "submitted as", fil.filename, "to temporary file", fil.stream.name]))
# Do whatever with stored file at `fil.stream.name`
return 'OK', 200
You can send any file via post api while calling the API just need to mention files={'any_key': fobj}
import requests
import json
url = "https://request-url.com"
headers = {"Content-Type": "application/json; charset=utf-8"}
with open(filepath, 'rb') as fobj:
response = requests.post(url, headers=headers, files={'file': fobj})
print("Status Code", response.status_code)
print("JSON Response ", response.json())
#martijn-pieters answer is correct, however I wanted to add a bit of context to data= and also to the other side, in the Flask server, in the case where you are trying to upload files and a JSON.
From the request side, this works as Martijn describes:
files = {'upload_file': open('file.txt','rb')}
values = {'DB': 'photcat', 'OUT': 'csv', 'SHORT': 'short'}
r = requests.post(url, files=files, data=values)
However, on the Flask side (the receiving webserver on the other side of this POST), I had to use form
#app.route("/sftp-upload", methods=["POST"])
def upload_file():
if request.method == "POST":
# the mimetype here isnt application/json
# see here: https://stackoverflow.com/questions/20001229/how-to-get-posted-json-in-flask
body = request.form
print(body) # <- immutable dict
body = request.get_json() will return nothing. body = request.get_data() will return a blob containing lots of things like the filename etc.
Here's the bad part: on the client side, changing data={} to json={} results in this server not being able to read the KV pairs! As in, this will result in a {} body above:
r = requests.post(url, files=files, json=values). # No!
This is bad because the server does not have control over how the user formats the request; and json= is going to be the habbit of requests users.
Upload:
with open('file.txt', 'rb') as f:
files = {'upload_file': f.read()}
values = {'DB': 'photcat', 'OUT': 'csv', 'SHORT': 'short'}
r = requests.post(url, files=files, data=values)
Download (Django):
with open('file.txt', 'wb') as f:
f.write(request.FILES['upload_file'].file.read())
Regarding the answers given so far, there was always something missing that prevented it to work on my side. So let me show you what worked for me:
import json
import os
import requests
API_ENDPOINT = "http://localhost:80"
access_token = "sdfJHKsdfjJKHKJsdfJKHJKysdfJKHsdfJKHs" # TODO: get fresh Token here
def upload_engagement_file(filepath):
url = API_ENDPOINT + "/api/files" # add any URL parameters if needed
hdr = {"Authorization": "Bearer %s" % access_token}
with open(filepath, "rb") as fobj:
file_obj = fobj.read()
file_basename = os.path.basename(filepath)
file_to_upload = {"file": (str(file_basename), file_obj)}
finfo = {"fullPath": filepath}
upload_response = requests.post(url, headers=hdr, files=file_to_upload, data=finfo)
fobj.close()
# print("Status Code ", upload_response.status_code)
# print("JSON Response ", upload_response.json())
return upload_response
Note that requests.post(...) needs
a url parameter, containing the full URL of the API endpoint you're calling, using the API_ENDPOINT, assuming we have an http://localhost:8000/api/files endpoint to POST a file
a headers parameter, containing at least the authorization (bearer token)
a files parameter taking the name of the file plus the entire file content
a data parameter taking just the path and file name
Installation required (console):
pip install requests
What you get back from the function call is a response object containing a status code and also the full error message in JSON format. The commented print statements at the end of upload_engagement_file are showing you how you can access them.
Note: Some useful additional information about the requests library can be found here
Some may need to upload via a put request and this is slightly different that posting data. It is important to understand how the server expects the data in order to form a valid request. A frequent source of confusion is sending multipart-form data when it isn't accepted. This example uses basic auth and updates an image via a put request.
url = 'foobar.com/api/image-1'
basic = requests.auth.HTTPBasicAuth('someuser', 'password123')
# Setting the appropriate header is important and will vary based
# on what you upload
headers = {'Content-Type': 'image/png'}
with open('image-1.png', 'rb') as img_1:
r = requests.put(url, auth=basic, data=img_1, headers=headers)
While the requests library makes working with http requests a lot easier, some of its magic and convenience obscures just how to craft more nuanced requests.
In Ubuntu you can apply this way,
to save file at some location (temporary) and then open and send it to API
path = default_storage.save('static/tmp/' + f1.name, ContentFile(f1.read()))
path12 = os.path.join(os.getcwd(), "static/tmp/" + f1.name)
data={} #can be anything u want to pass along with File
file1 = open(path12, 'rb')
header = {"Content-Disposition": "attachment; filename=" + f1.name, "Authorization": "JWT " + token}
res= requests.post(url,data,header)

send email with a pandas dataframe as attachment

I have a pandas dataframe that I am looking to convert into an xlsx and attach to an email. I can send emails based on outlook(this is the only way I can do it). I am able to convert the dataframe into an xlsx and save it on my drive and afterwards send it as attachment but I am looking to attach it directly without having to save it on my drive. see bellow my function to send emails:
def email():
olMailItem = 0x0
obj = win32com.client.Dispatch("Outlook.Application")
newMail = obj.CreateItem(olMailItem)
newMail.Subject ="FRANCE SO"
newMail.Body =' '
newMail.To = "email adress"
newMail.Attachments.Add(attachment)
newMail.Send()
return
attachment is the dataframe that has been transformed into xlsx
Have you tried to play with the io module in Python 3? It allows you to use streams as file-like objects, so that APIs that expect a file can read from or save their content to the stream instead.
That works nicely, using a StringIO along with pandas.DataFrame.to_csv:
import io
def export_csv(df):
with io.StringIO() as buffer:
df.to_csv(buffer)
return buffer.getvalue()
That works, because to_csv expects a string (interpreted as a path) or a file handle, and StringIO can be used like a file handle. Unfortunately, pandas.DataFrame.to_excel works with either a string (interpreted as a path) or an ExcelWriter. In that case, we need to create the ExcelWriter ourselves, and wrap a BytesIO with it.
import io
import pandas as pd
def export_excel(df):
with io.BytesIO() as buffer:
with pd.ExcelWriter(buffer) as writer:
df.to_excel(writer)
return buffer.getvalue()
I am not familiar with the Outlook Python tools for sending emails, I use SMTP:
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
SEND_FROM = 'noreply#example.com'
EXPORTERS = {'dataframe.csv': export_csv, 'dataframe.xlsx': export_excel}
def send_dataframe(send_to, subject, body, df):
multipart = MIMEMultipart()
multipart['From'] = SEND_FROM
multipart['To'] = send_to
multipart['Subject'] = subject
for filename in EXPORTERS:
attachment = MIMEApplication(EXPORTERS[filename](df))
attachment['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
multipart.attach(attachment)
multipart.attach(MIMEText(body, 'html'))
s = smtplib.SMTP('localhost')
s.sendmail(SEND_FROM, send_to, multipart.as_string())
s.quit()
I hope this helps, good luck!

poplib Python base64 content encoded twice?

I am trying to read content from Gmail using poplib. The content in my email is mostly base64 encoded but contains some additional symbols. However, when I read the content using poplib, for some reason my original content is base64 encoded again.
Example content in my email: {{{eyJjb250ZW50IjpbeyJjZWxsIjoiQTEiLCJ2YW
Example response that I get from poplib: e3t7ZXlKamIyNTBaVzUwSWpwYmV5SmpaV3hzSWpvaVFURWlMQ0oyWVd
Any suggestions on how to retrieve my original (raw) content (without poplib encoding it again) will be greatly appreciated.
Here is the code that I am using:
messages = [pop_conn.retr(i) for i in range(1, len(pop_conn.list()[1]) + 1)]
psr = parser.FeedParser()
for msg in messages:
psr.feed(str(msg))
mess = psr.close()
payload = mess.get_payload(decode=True)

Categories

Resources