Downloading word file - python docx package

Hi VIKTOR-team,

Iโ€™d like to ask a question regarding downloading a word file. We have internally developed code to automatically build reports/factsheets/appendices from scratch using the docx package. I am struggling with how properly download this docx-file on the VIKTOR platform.

To start with, this is the relevant importing-part:

from docx import Document
from internal_arcadis_module import generate_factsheet

The method below sits in my controller class and is triggered with a DownloadButton:

def export_stix_factsheet_word(self, params, **kwargs):
        stix_file_resources: List[FileResource] = params.tab_1.input_section.stix_files
        (...)
        arcadis_factsheet = generate_factsheet.factsheet_of_stix_dir(...)
        # This returns a Document() from python docx package
        file_arc_factsheet = arc_factsheet_to_viktor_file(arc_factsheet=arcadis_factsheet ) 
        # This returns a File() from viktor.core package
        now = datetime.datetime.now()
        return DownloadResult(file_arc_factsheet,
                              f'factsheet_{params.tab_1.factsheet_selection.factsheet_name}_{now}.docx')

The method arc_factsheet_to_viktor_file is as follows and makes use of a temporary path to store the docx-file:

def arc_factsheet_to_viktor_file(arcadis_factsheet: Document) -> File:
    temp_file = NamedTemporaryFile(suffix='.docx', delete=False,
                                   mode='wb')
    temp_file.close()
    path_temp_file = Path(temp_file.name)
    # save factsheet at path_temp_file
    factsheet = arcadis_factsheet.save(path_temp_file)
    # make a File-object, with factsheet contents from temp file
    file = File.from_path(Path(path_temp_file))
    os.remove(temp_file.name)
    return file

The problem is when the temporary file is removed by os.remove(temp_file.name), the code gives an error (see below). It works when the line is commented, but I feel that is not what I want because the temporary file will sit in my local folder forever.

2022-07-11 13:27:07.701 INFO    : Job (uid: 505) received - EntityType: MyEntityType - function: export_stix_factsheet_word
     2022-07-11 13:27:10.558 ERROR   : Exception is raised
     Traceback (most recent call last):
       File "viktor_connector\connector.pyx", line 400, in connector.Job.execute
       File "viktor\core.pyx", line 1799, in viktor.core._handle_job
       File "viktor\core.pyx", line 1783, in viktor.core._handle_job.non_view_result
       File "viktor\result.pyx", line 133, in viktor.result.DownloadResult._serialize
       File "viktor\core.pyx", line 899, in viktor.core.File.open_binary
       File "viktor\core.pyx", line 706, in viktor.core._PathFileManager.create_binary_file
       File "viktor\core.pyx", line 459, in viktor.core._BinaryPathFile.__init__
     FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\...\\AppData\\Local\\Temp\\tmpwk4sqf85.docx'
     2022-07-11 13:27:37.796 INFO    : Reloading app...

My question is how to properly save and download the file? Your help is very much appreciated.

Hi Yida,

I donโ€™t immediately see what is the problem. I found a similar snipped using TemporaryFile that works fine in another app, that I added below. Maybe that will help you to spot the difference?

I am not sure, but it is possible the viktor File is trying to be efficient, and object only downloads the contents when they are needed. In that case, it only looks up the content by the time you click on download. but by then you have already removed the file at the specified path. That would explain why it works when you do not remove the file.

It may be that you edited your snippet a bit for simplicity, but I notice that you do not use the variable factsheet in the arc_factsheet_to_viktor_file function. In case that contains your desired file as bytes, you could put that in the DownloadResult as is, or create the viktor File.from_data() instead.

        temp_file = NamedTemporaryFile(suffix=".fod", delete=False, mode="wb")
        temp_file.write(self.file_content.encode("utf-8"))
        temp_file.close()
        path_out = Path(temp_file.name)
        model_to_be_set.parse(path_out)  # parse the output of the model in-place
        os.remove(temp_file.name)
1 Like

Hi Yida,

In this case i would recommend to use a Bytes Buffer (BytesIO()) instead of a NamedTempFile()

The difference is that Tempfile is saved on the HDD/SDD, where a BytesIO buffer is kept only in RAM memory. A BytesIO object can be seen as a file only existing in RAM memory.

from io import BytesIO

def arc_factsheet_to_viktor_file(arcadis_factsheet: Document) -> File:

    # Create empty Bytes buffer
    bytes_buffer = BytesIO()

    # save file in the Bytes buffer
    factsheet = arcadis_factsheet.save(bytes_buffer)

    # Create VIKTOR File object from Bytes buffer
    file = File.from_data(bytes_buffer.getvalue())

    return file

I hope this helps.

edited after @yida.tao her comment

1 Like

Hi Maarten,

Thanks for your reply. I was indeed looking for something else instead of the NamedTempFile().
However, when I use your solution, I get an error and I donโ€™t understand the traceback

2022-07-14 15:46:00.451 ERROR   : Exception is raised
Traceback (most recent call last):
  File "viktor_connector\connector.pyx", line 400, in connector.Job.execute
  File "viktor\core.pyx", line 1799, in viktor.core._handle_job
  File "viktor\core.pyx", line 1783, in viktor.core._handle_job.non_view_result
  File "viktor\result.pyx", line 133, in viktor.result.DownloadResult._serialize
  File "viktor\core.pyx", line 899, in viktor.core.File.open_binary
  File "viktor\core.pyx", line 685, in viktor.core._DataFileManager.create_binary_file
TypeError: Argument 'data' has incorrect type (expected bytes, got _io.BytesIO)

I would guess that File.from_data is giving trouble somewhere. Could you please help me further?

Sorry my mistake. The line should be:

    file = File.from_data(bytes_buffer.getvalue())

The .getvalue() function returns the bytes in the BytesIO buffer.
I have adjusted the original message.