Skip to content

Commit 9befca9

Browse files
committed
[IMP] Minimizes memory consumption
1 parent 92f74f3 commit 9befca9

2 files changed

Lines changed: 114 additions & 77 deletions

File tree

report_py3o/models/py3o_report.py

Lines changed: 106 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
import json
99
import logging
1010
import os
11+
from contextlib import closing
12+
1113
import pkg_resources
1214
import requests
1315
import sys
14-
from tempfile import NamedTemporaryFile
16+
import tempfile
1517
from zipfile import ZipFile, ZIP_DEFLATED
1618

19+
from openerp.exceptions import AccessError
1720
from openerp.exceptions import UserError
1821
from openerp.report.report_sxw import rml_parse
1922
from openerp import api, fields, models, _
@@ -149,49 +152,72 @@ def _get_parser_context(self, model_instance, data):
149152
self._extend_parser_context(context_instance, report_xml)
150153
return context_instance.localcontext
151154

152-
@api.multi
153-
def _postprocess_report(self, content, res_id, save_in_attachment):
155+
@api.model
156+
def _get_report_from_name(self, report_name):
157+
"""Get the first record of ir.actions.report.xml having the
158+
``report_name`` as value for the field report_name.
159+
"""
160+
res = super(Py3oReport, self)._get_report_from_name(report_name)
161+
if res:
162+
return res
163+
# maybe a py3o reprot
164+
report_obj = self.env['ir.actions.report.xml']
165+
return report_obj.search(
166+
[('report_type', '=', 'py3o'),
167+
('report_name', '=', report_name)])
168+
169+
@api.model
170+
def _postprocess_report(self, report_path, res_id, save_in_attachment):
154171
if save_in_attachment.get(res_id):
155-
attachment = {
156-
'name': save_in_attachment.get(res_id),
157-
'datas': base64.encodestring(content),
158-
'datas_fname': save_in_attachment.get(res_id),
159-
'res_model': save_in_attachment.get('model'),
160-
'res_id': res_id,
161-
}
162-
return self.env['ir.attachment'].create(attachment)
163-
return False
172+
with open(report_path, 'rb') as pdfreport:
173+
attachment = {
174+
'name': save_in_attachment.get(res_id),
175+
'datas': base64.encodestring(pdfreport.read()),
176+
'datas_fname': save_in_attachment.get(res_id),
177+
'res_model': save_in_attachment.get('model'),
178+
'res_id': res_id,
179+
}
180+
try:
181+
self.env['ir.attachment'].create(attachment)
182+
except AccessError:
183+
logger.info("Cannot save PDF report %r as attachment",
184+
attachment['name'])
185+
else:
186+
logger.info(
187+
'The PDF document %s is now saved in the database',
188+
attachment['name'])
164189

165190
@api.multi
166191
def _create_single_report(self, model_instance, data, save_in_attachment):
167192
""" This function to generate our py3o report
168193
"""
169194
self.ensure_one()
170195
report_xml = self.ir_actions_report_xml_id
171-
196+
filetype = report_xml.py3o_filetype
197+
result_fd, result_path = tempfile.mkstemp(
198+
suffix='.' + filetype, prefix='p3o.report.tmp.')
172199
tmpl_data = self.get_template()
173200

174201
in_stream = StringIO(tmpl_data)
175-
out_stream = StringIO()
176-
template = Template(in_stream, out_stream, escape_false=True)
177-
localcontext = self._get_parser_context(model_instance, data)
178-
if report_xml.py3o_is_local_fusion:
179-
template.render(localcontext)
180-
in_stream = out_stream
181-
datadict = {}
182-
else:
183-
expressions = template.get_all_user_python_expression()
184-
py_expression = template.convert_py3o_to_python_ast(expressions)
185-
convertor = Py3oConvertor()
186-
data_struct = convertor(py_expression)
187-
datadict = data_struct.render(localcontext)
188-
189-
filetype = report_xml.py3o_filetype
190-
is_native = Formats().get_format(filetype).native
191-
if is_native:
192-
res = out_stream.getvalue()
193-
else: # Call py3o.server to render the template in the desired format
194-
in_stream.seek(0)
202+
with closing(os.fdopen(result_fd, 'w+')) as out_stream:
203+
template = Template(in_stream, out_stream, escape_false=True)
204+
localcontext = self._get_parser_context(model_instance, data)
205+
is_native = Formats().get_format(filetype).native
206+
if report_xml.py3o_is_local_fusion:
207+
template.render(localcontext)
208+
out_stream.seek(0)
209+
in_stream = out_stream.read()
210+
datadict = {}
211+
else:
212+
expressions = template.get_all_user_python_expression()
213+
py_expression = template.convert_py3o_to_python_ast(
214+
expressions)
215+
convertor = Py3oConvertor()
216+
data_struct = convertor(py_expression)
217+
datadict = data_struct.render(localcontext)
218+
219+
if not is_native or not report_xml.py3o_is_local_fusion:
220+
# Call py3o.server to render the template in the desired format
195221
files = {
196222
'tmpl_file': in_stream,
197223
}
@@ -210,21 +236,13 @@ def _create_single_report(self, model_instance, data, save_in_attachment):
210236
_('Fusion server error %s') % r.text,
211237
)
212238

213-
# Here is a little joke about Odoo
214-
# we do nice chunked reading from the network...
215239
chunk_size = 1024
216-
with NamedTemporaryFile(
217-
suffix=filetype,
218-
prefix='py3o-template-'
219-
) as fd:
240+
with open(result_path, 'w+') as fd:
220241
for chunk in r.iter_content(chunk_size):
221242
fd.write(chunk)
222-
fd.seek(0)
223-
# ... but odoo wants the whole data in memory anyways :)
224-
res = fd.read()
225243
self._postprocess_report(
226-
res, model_instance.id, save_in_attachment)
227-
return res, "." + self.ir_actions_report_xml_id.py3o_filetype
244+
result_path, model_instance.id, save_in_attachment)
245+
return result_path
228246

229247
@api.multi
230248
def _get_or_create_single_report(self, model_instance, data,
@@ -239,43 +257,42 @@ def _get_or_create_single_report(self, model_instance, data,
239257
model_instance, data, save_in_attachment)
240258

241259
@api.multi
242-
def _zip_results(self, results):
260+
def _zip_results(self, reports_path):
243261
self.ensure_one()
244262
zfname_prefix = self.ir_actions_report_xml_id.name
245-
with NamedTemporaryFile(suffix="zip", prefix='py3o-zip-result') as fd:
246-
with ZipFile(fd, 'w', ZIP_DEFLATED) as zf:
247-
cpt = 0
248-
for r, ext in results:
249-
fname = "%s_%d.%s" % (zfname_prefix, cpt, ext)
250-
zf.writestr(fname, r)
251-
cpt += 1
252-
fd.seek(0)
253-
return fd.read(), 'zip'
263+
result_path = tempfile.mktemp(suffix="zip", prefix='py3o-zip-result')
264+
with ZipFile(result_path, 'w', ZIP_DEFLATED) as zf:
265+
cpt = 0
266+
for report in reports_path:
267+
fname = "%s_%d.%s" % (
268+
zfname_prefix, cpt, report.split('.')[-1])
269+
zf.write(report, fname)
254270

255-
@api.multi
256-
def _merge_pdfs(self, results):
257-
from pyPdf import PdfFileWriter, PdfFileReader
258-
output = PdfFileWriter()
259-
for r in results:
260-
reader = PdfFileReader(StringIO(r[0]))
261-
for page in range(reader.getNumPages()):
262-
output.addPage(reader.getPage(page))
263-
s = StringIO()
264-
output.write(s)
265-
return s.getvalue(), formats.FORMAT_PDF
271+
cpt += 1
272+
return result_path
266273

267274
@api.multi
268-
def _merge_results(self, results):
275+
def _merge_results(self, reports_path):
269276
self.ensure_one()
270-
if not results:
271-
return False, False
272-
if len(results) == 1:
273-
return results[0]
274277
filetype = self.ir_actions_report_xml_id.py3o_filetype
278+
if not reports_path:
279+
return False, False
280+
if len(reports_path) == 1:
281+
return reports_path[0], filetype
275282
if filetype == formats.FORMAT_PDF:
276-
return self._merge_pdfs(results)
283+
return self._merge_pdf(reports_path), formats.FORMAT_PDF
277284
else:
278-
return self._zip_results(results)
285+
return self._zip_results(reports_path), 'zip'
286+
287+
@api.model
288+
def _cleanup_tempfiles(self, temporary_files):
289+
# Manual cleanup of the temporary files
290+
for temporary_file in temporary_files:
291+
try:
292+
os.unlink(temporary_file)
293+
except (OSError, IOError):
294+
logger.error(
295+
'Error when trying to remove file %s' % temporary_file)
279296

280297
@api.multi
281298
def create_report(self, res_ids, data):
@@ -285,8 +302,21 @@ def create_report(self, res_ids, data):
285302
res_ids)
286303
save_in_attachment = self._check_attachment_use(
287304
model_instances, self.ir_actions_report_xml_id) or {}
288-
results = []
305+
reports_path = []
289306
for model_instance in model_instances:
290-
results.append(self._get_or_create_single_report(
291-
model_instance, data, save_in_attachment))
292-
return self._merge_results(results)
307+
reports_path.append(
308+
self._get_or_create_single_report(
309+
model_instance, data, save_in_attachment))
310+
311+
result_path, filetype = self._merge_results(reports_path)
312+
reports_path.append(result_path)
313+
314+
# Here is a little joke about Odoo
315+
# we do all the generation process using files to avoid memory
316+
# consumption...
317+
# ... but odoo wants the whole data in memory anyways :)
318+
319+
with open(result_path, 'r+b') as fd:
320+
res = fd.read()
321+
self._cleanup_tempfiles(set(reports_path))
322+
return res, filetype

report_py3o/tests/test_report_py3o.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import mock
66
import os
77
import pkg_resources
8+
import tempfile
89

910
from py3o.formats import Formats
1011

@@ -60,11 +61,17 @@ def test_reports(self):
6061
report = self.env.ref("report_py3o.res_users_report_py3o")
6162
with mock.patch.object(
6263
py3o_report.__class__, '_create_single_report') as patched_pdf:
64+
result = tempfile.mktemp('.txt')
65+
with open(result, 'w') as fp:
66+
fp.write('dummy')
67+
patched_pdf.return_value = result
6368
# test the call the the create method inside our custom parser
6469
report.render_report(self.env.user.ids,
6570
report.report_name,
6671
{})
6772
self.assertEqual(1, patched_pdf.call_count)
73+
# generated files no more exists
74+
self.assertFalse(os.path.exists(result))
6875
res = report.render_report(
6976
self.env.user.ids, report.report_name, {})
7077
self.assertTrue(res)
@@ -79,7 +86,7 @@ def test_reports(self):
7986
magick_response.iter_content.return_value = "test result"
8087
res = report.render_report(
8188
self.env.user.ids, report.report_name, {})
82-
self.assertEqual(('test result', '.pdf'), res)
89+
self.assertEqual(('test result', 'pdf'), res)
8390

8491
def test_report_template_configs(self):
8592
report = self.env.ref("report_py3o.res_users_report_py3o")

0 commit comments

Comments
 (0)