Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to generate a DOCX in Python and save it in memory?

I have a task of generating a DOCX file from a template and then serving it via Flask. I use python-docx-templates which is simply a wrapper around python-docx allowing for use of jinja templates.

In the end they suggest using StringIO to save file only in memory, so this is how my code looks like:

def report_doc(user_id):
    # Prepare the data...

    from docxtpl import DocxTemplate

    doc = DocxTemplate(app.root_path+'/templates/report.docx')
    doc.render({
        # Pass parameters
    })
    from io import StringIO
    file_stream = StringIO()
    doc.save(file_stream)

    return send_file(file_stream, as_attachment=True, attachment_filename='report_'+user_id+'.docx')

On saving it throws an error TypeError: string argument expected, got 'bytes'. After googling it, I found this answer which says that ZipFile expects BytesIO. However, when I substituted StringIO with BytesIO, it only returned an empty file, so it doesn't throw any error, but definitely doesn't save the file.

What exactly would work in this case? If something is entirely wrong here, how in general could this work?

Thank you!

UPD: Here's the exception with full trace to the save function call:

File "/ms/controllers.py", line 1306, in report_doc
    doc.save(file_stream)
  File "/.env/lib/python3.5/site-packages/docx/document.py", line 142, in save
    self._part.save(path_or_stream)
  File "/.env/lib/python3.5/site-packages/docx/parts/document.py", line 129, in save
    self.package.save(path_or_stream)
  File "/.env/lib/python3.5/site-packages/docx/opc/package.py", line 160, in save
    PackageWriter.write(pkg_file, self.rels, self.parts)
  File "/.env/lib/python3.5/site-packages/docx/opc/pkgwriter.py", line 33, in write
    PackageWriter._write_content_types_stream(phys_writer, parts)
  File "/.env/lib/python3.5/site-packages/docx/opc/pkgwriter.py", line 45, in _write_content_types_stream
    phys_writer.write(CONTENT_TYPES_URI, cti.blob)
  File "/.env/lib/python3.5/site-packages/docx/opc/phys_pkg.py", line 155, in write
    self._zipf.writestr(pack_uri.membername, blob)
  File "/usr/lib/python3.5/zipfile.py", line 1581, in writestr
    self.fp.write(zinfo.FileHeader(zip64))
TypeError: string argument expected, got 'bytes'
like image 619
JaffParker Avatar asked Sep 02 '17 06:09

JaffParker


People also ask

Does python docx work with Doc?

docx Module. Python docx module allows users to manipulate docs by either manipulating the existing one or creating a new empty document and manipulating it.

How does python docx work?

python-docx allows you to create new documents as well as make changes to existing ones. Actually, it only lets you make changes to existing documents; it's just that if you start with a document that doesn't have any content, it might feel at first like you're creating one from scratch.


1 Answers

Using a BytesIO instance is correct, but you need to rewind the file pointer before passing it to send_file:

Make sure that the file pointer is positioned at the start of data to send before calling send_file().

So this should work:

import io
from docxtpl import DocxTemplate

def report_doc(user_id):
   # Prepare the data...

   doc = DocxTemplate(app.root_path+'/templates/report.docx')
   doc.render({
        # Pass parameters
   })

   # Create in-memory buffer
   file_stream = io.BytesIO()
   # Save the .docx to the buffer
   doc.save(file_stream)
   # Reset the buffer's file-pointer to the beginning of the file
   file_stream.seek(0)

   return send_file(file_stream, as_attachment=True, attachment_filename='report_'+user_id+'.docx')

(Testing on Firefox, I found the browser kept retrieving the file from cache even if I specified a different filename, so you may need to clear your browser's cache while testing, or disable caching in dev tools if your browser supports this, or adjust Flask's cache control settings).

like image 93
snakecharmerb Avatar answered Oct 26 '22 01:10

snakecharmerb