Skip to content

Commit

Permalink
Merge pull request #15 from cloudblue/feature/LITE-17919_refactor_bas…
Browse files Browse the repository at this point in the history
…e_renderer

LITE-17919: Refactor Base Renderer
  • Loading branch information
ffaraoneim authored Mar 24, 2021
2 parents 720680d + 64bbd3e commit c1483bf
Show file tree
Hide file tree
Showing 15 changed files with 181 additions and 314 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build Connect Reports Renderers
name: Build Connect Reports Core

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Connect Reports Core

![pyversions](https://img.shields.io/pypi/pyversions/connect-reports-core.svg) [![Build Connect Reports Core](https://github.com/cloudblue/connect-reports-core/actions/workflows/build.yml/badge.svg)](https://github.com/cloudblue/connect-reports-core/actions/workflows/build.yml)[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=alert_status)](https://sonarcloud.io/dashboard?id=connect-reports-core) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=coverage)](https://sonarcloud.io/dashboard?id=connect-reports-core) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=connect-reports-core)
![pyversions](https://img.shields.io/pypi/pyversions/connect-reports-core.svg) [![PyPi Status](https://img.shields.io/pypi/v/connect-reports-core.svg)](https://pypi.org/project/connect-reports-core/) [![Build Connect Reports Core](https://github.com/cloudblue/connect-reports-core/actions/workflows/build.yml/badge.svg)](https://github.com/cloudblue/connect-reports-core/actions/workflows/build.yml) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=alert_status)](https://sonarcloud.io/dashboard?id=connect-reports-core) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=coverage)](https://sonarcloud.io/dashboard?id=connect-reports-core) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=connect-reports-core&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=connect-reports-core)

## Introduction

Expand Down
51 changes: 49 additions & 2 deletions connect/reports/renderers/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Copyright © 2021 CloudBlue. All rights reserved.

from abc import ABCMeta, abstractmethod
import zipfile
import os
from datetime import datetime
import tempfile
import json

import pytz


class BaseRenderer(metaclass=ABCMeta):
Expand Down Expand Up @@ -34,9 +41,49 @@ def get_context(self, data):
def set_extra_context(self, data):
self.extra_context = data

def render(self, data, output_file, start_time=None):
start_time = start_time or datetime.now(tz=pytz.utc)
with tempfile.TemporaryDirectory() as tmpdir:
report_file = self.generate_report(data, f'{tmpdir}/report')
summary_file = self.generate_summary(f'{tmpdir}/summary', start_time)
pack_file = self.pack_files(report_file, summary_file, output_file)
return pack_file

def generate_summary(self, output_file, start_time):
data = {
'title': 'Report Execution Information',
'data': {
'report_start_time': start_time.isoformat(),
'report_finish_time': datetime.now(tz=pytz.utc).isoformat(),
'account_id': self.account.id,
'account_name': self.account.name,
'report_id': self.report.id,
'report_name': self.report.name,
'runtime_environment': self.environment,
'report_execution_parameters': json.dumps(
self.report.values,
indent=4,
sort_keys=True,
),
},
}
output_file = f'{output_file}.json'
json.dump(data, open(output_file, 'w'))
return output_file

def pack_files(self, report_file, summary_file, output_file):
tokens = output_file.split('.')
if tokens[-1] != 'zip':
output_file = f'{tokens[0]}.zip'
with zipfile.ZipFile(output_file, 'w') as repzip:
repzip.write(report_file, os.path.basename(report_file))
repzip.write(summary_file, os.path.basename(summary_file))

return output_file

@abstractmethod
def render(self, data, output_file):
raise NotImplementedError('Subclasses must implement the `render` method.')
def generate_report(self, data, output_file):
raise NotImplementedError('Subclasses must implement the `generate_report` method.')

@classmethod
def validate(cls, definition):
Expand Down
46 changes: 6 additions & 40 deletions connect/reports/renderers/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,17 @@
from connect.reports.renderers.base import BaseRenderer
from connect.reports.renderers.registry import register

from datetime import datetime
import tempfile
import zipfile
import os
import csv

import pytz


@register('csv')
class CSVRenderer(BaseRenderer):
def render(self, data, output_file):
start_time = datetime.now(tz=pytz.utc)

with tempfile.TemporaryDirectory() as tmpdir:
report_file = f'{tmpdir}/report.csv'
with open(report_file, 'w') as fp:
writer = csv.writer(fp, delimiter=';', quotechar='"', quoting=csv.QUOTE_ALL)
for row in data:
writer.writerow(row)

summary_file = self._add_info_csv(start_time, f'{tmpdir}/summary.csv')

output_file = f'{output_file}.zip'
with zipfile.ZipFile(output_file, 'w') as repzip:
repzip.write(report_file, os.path.basename(report_file))
repzip.write(summary_file, os.path.basename(summary_file))

return output_file

def _add_info_csv(self, start_time, output_file):
values = []
values.append(('', 'Report Execution Information'))
values.append(('report_start_time', f'{start_time.isoformat()}'))
values.append(('report_finish_time', f'{datetime.now(tz=pytz.utc).isoformat()}'))
values.append(('account_id', f'{self.account.id}'))
values.append(('account_name', f'{self.account.name}'))
values.append(('report_id', f'{self.report.id}'))
values.append(('report_name', f'{self.report.name}'))
values.append(('runtime_environment', f'{self.environment}'))
values.append(('report_execution_parameters', f'{self.report.values}'))

def generate_report(self, data, output_file):
tokens = output_file.split('.')
if tokens[-1] != 'csv':
output_file = f'{tokens[0]}.csv'
with open(output_file, 'w') as fp:
writer = csv.writer(fp, delimiter=';', quotechar='"', quoting=csv.QUOTE_ALL)
for value in values:
writer.writerow(value)

for row in data:
writer.writerow(row)
return output_file
45 changes: 4 additions & 41 deletions connect/reports/renderers/j2.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
# Copyright © 2021 CloudBlue. All rights reserved.

import os
import json
from datetime import datetime
import tempfile
import zipfile

import pytz

from jinja2 import (
Environment,
Expand All @@ -20,7 +14,7 @@

@register('jinja2')
class Jinja2Renderer(BaseRenderer):
def render(self, data, output_file):
def generate_report(self, data, output_file):
path, name = self.template.rsplit('/', 1)
loader = FileSystemLoader(os.path.join(self.root_dir, path))
env = Environment(
Expand All @@ -30,40 +24,9 @@ def render(self, data, output_file):
template = env.get_template(name)
_, ext, _ = name.rsplit('.', 2)

start_time = datetime.now(tz=pytz.utc)
with tempfile.TemporaryDirectory() as tmpdir:
report_file = f'{tmpdir}/report.{ext}'
template.stream(self.get_context(data)).dump(open(report_file, 'w'))

summary_file = self._add_info_json(start_time, f'{tmpdir}/summary.json')

output_file = f'{output_file}.zip'
with zipfile.ZipFile(output_file, 'w') as repzip:
repzip.write(report_file, os.path.basename(report_file))
repzip.write(summary_file, os.path.basename(summary_file))

return output_file

def _add_info_json(self, start_time, output_file):
data = {
'title': 'Report Execution Information',
'data': {
'report_start_time': start_time.isoformat(),
'report_finish_time': datetime.now(tz=pytz.utc).isoformat(),
'account_id': self.account.id,
'account_name': self.account.name,
'report_id': self.report.id,
'report_name': self.report.name,
'runtime_environment': self.environment,
'report_execution_parameters': json.dumps(
self.report.values,
indent=4,
sort_keys=True,
),
},
}
json.dump(data, open(output_file, 'w'))
return output_file
report_file = f'{output_file}.{ext}'
template.stream(self.get_context(data)).dump(open(report_file, 'w'))
return report_file

@classmethod
def validate(cls, definition):
Expand Down
43 changes: 4 additions & 39 deletions connect/reports/renderers/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,14 @@
from connect.reports.renderers.base import BaseRenderer
from connect.reports.renderers.registry import register

from datetime import datetime
import json
import tempfile
import zipfile
import os

import pytz


@register('json')
class JSONRenderer(BaseRenderer):
def render(self, data, output_file):
start_time = datetime.now(tz=pytz.utc)

with tempfile.TemporaryDirectory() as tmpdir:
report_file = f'{tmpdir}/report.json'
json.dump(data, open(report_file, 'w'))
summary_file = self._add_info_json(start_time, f'{tmpdir}/summary.json')

output_file = f'{output_file}.zip'
with zipfile.ZipFile(output_file, 'w') as repzip:
repzip.write(report_file, os.path.basename(report_file))
repzip.write(summary_file, os.path.basename(summary_file))

return output_file

def _add_info_json(self, start_time, output_file):
data = {
'title': 'Report Execution Information',
'data': {
'report_start_time': start_time.isoformat(),
'report_finish_time': datetime.now(tz=pytz.utc).isoformat(),
'account_id': self.account.id,
'account_name': self.account.name,
'report_id': self.report.id,
'report_name': self.report.name,
'runtime_environment': self.environment,
'report_execution_parameters': json.dumps(
self.report.values,
indent=4,
sort_keys=True,
),
},
}
def generate_report(self, data, output_file):
tokens = output_file.split('.')
if tokens[-1] != 'json':
output_file = f'{tokens[0]}.json'
json.dump(data, open(output_file, 'w'))
return output_file
18 changes: 6 additions & 12 deletions connect/reports/renderers/pdf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Copyright © 2021 CloudBlue. All rights reserved.

import os
import shutil
from functools import partial

from weasyprint import HTML, default_url_fetcher
Expand Down Expand Up @@ -30,24 +29,19 @@ def local_fetcher(url, root_dir=None, template_dir=None):

@register('pdf')
class PDFRenderer(Jinja2Renderer):
def render(self, data, output_file):
rendered_file = super().render(data, output_file)
temp_file = f'{output_file}.temp'
shutil.move(
rendered_file,
temp_file,
)
def generate_report(self, data, output_file):
tokens = output_file.split('.')
if tokens[-1] != 'pdf':
output_file = f'{tokens[0]}.pdf'

rendered_file = super().generate_report(data, output_file)
fetcher = partial(
local_fetcher,
root_dir=self.root_dir,
template_dir=os.path.dirname(self.template),
)

html = HTML(filename=temp_file, url_fetcher=fetcher)
output_file = f'{output_file}.pdf'
html = HTML(filename=rendered_file, url_fetcher=fetcher)
html.write_pdf(output_file)
os.unlink(temp_file)
return output_file

@classmethod
Expand Down
12 changes: 8 additions & 4 deletions connect/reports/renderers/xlsx.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@

@register('xlsx')
class XLSXRenderer(BaseRenderer):
def render(self, data, output_file):
start_time = datetime.now(tz=pytz.utc)
def render(self, data, output_file, start_time):
self.start_time = start_time or datetime.now(tz=pytz.utc)
return self.generate_report(data, output_file)

def generate_report(self, data, output_file):
start_col_idx = self.args.get('start_col', 1)
row_idx = self.args.get('start_row', 2)
wb = load_workbook(
Expand All @@ -29,13 +32,14 @@ def render(self, data, output_file):
self.template,
),
)

ws = wb['Data']
for row in data:
for col_idx, cell_value in enumerate(row, start=start_col_idx):
ws.cell(row_idx, col_idx, value=cell_value)
row_idx += 1
self._add_info_sheet(wb.create_sheet('Info'), start_time)

self._add_info_sheet(wb.create_sheet('Info'), self.start_time)

output_file = f'{output_file}.xlsx'
wb.save(output_file)
return output_file
Expand Down
59 changes: 57 additions & 2 deletions tests/reports/renderers/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,64 @@

import pytest

from fs.tempfs import TempFS

from zipfile import ZipFile

from connect.reports.renderers.base import BaseRenderer
from connect.reports.datamodels import RendererDefinition


def test_base_render():
def test_generate_report():
with pytest.raises(NotImplementedError):
BaseRenderer.render(None, None, None)
BaseRenderer.generate_report(None, None, None)


@pytest.mark.parametrize(
('extra_context'),
(
{'name': 'test', 'desc': 'description'},
None,
),
)
def test_render(account_factory, report_factory, report_data, extra_context):

class DummyRenderer(BaseRenderer):
def generate_report(self, data, output_file):
output_file = f'{output_file}.ext'
open(output_file, 'w').write(str(data))
return output_file

tmp_fs = TempFS()
data = report_data(2, 2)
renderer = DummyRenderer(
'runtime',
tmp_fs.root_path,
account_factory(),
report_factory(),
)
renderer.set_extra_context(extra_context)
ctx = renderer.get_context(data)

output_file = renderer.render(data, f'{tmp_fs.root_path}/report')

assert output_file == f'{tmp_fs.root_path}/report.zip'
with ZipFile(output_file) as repzip:
assert sorted(repzip.namelist()) == ['report.ext', 'summary.json']
with repzip.open('report.ext') as repfile:
assert repfile.read().decode('utf-8') == str(data)

if extra_context:
assert 'name' in ctx['extra_context']
assert 'desc' in ctx['extra_context']


def test_validate_ok():
defs = RendererDefinition(
root_path='root_path',
id='renderer_id',
type='json',
description='description',
)

assert BaseRenderer.validate(defs) == []
Loading

0 comments on commit c1483bf

Please sign in to comment.