#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# This file is part of Mentat system (https://mentat.cesnet.cz/).
#
# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/)
# Use of this source is governed by the MIT license, see LICENSE file.
#-------------------------------------------------------------------------------
"""
This module contains base implementation of generic reporting class. It provides
common methods and utilities usefull for all kinds of reporters like:
* Jinja2 template rendering
* report localization
"""
__author__ = "Jan Mach <jan.mach@cesnet.cz>"
__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>"
import os
import time
import datetime
import gettext
import urllib.parse
import dateutil.parser
import jinja2
from babel.numbers import format_decimal
from babel.dates import format_datetime, format_timedelta, get_timezone, UTC
BABEL_RFC3339_FORMAT = "yyyy-MM-ddTHH:mm:ssZZZ"
"""
RFC3339 compliant time format specifier for Babel format_datetime function.
Resources: http://babel.pocoo.org/en/latest/dates.html#date-fields
"""
[docs]class BaseReporter:
"""
Implementation of base reporting class providing following features and
services:
* ``self.renderer`` - Jinja2 template rendering
* ``self.translator`` - report localization
"""
def __init__(self, logger, reports_dir, templates_dir, locale = 'en', timezone = 'UTC', translations_dir = None):
"""
:param str reports_dir: Name of the directory containing generated report files.
:param str templates_dir: Name of the directory containing report template files.
:param str locale: Locale for report rendering.
:param str timezone: Timezone for report rendering.
"""
self.logger = logger
self.reports_dir = reports_dir
self.templates_dir = templates_dir
self.translations_dir = translations_dir if translations_dir is not None else ";".join(os.path.join(dir, 'translations') for dir in templates_dir.split(";"))
self.locale = None
self.timezone = None
self.tzinfo = None
self.local_tz_name = time.localtime().tm_zone
self.local_tz_offset = datetime.timedelta(seconds = time.localtime().tm_gmtoff)
self.set_locale(locale)
self.set_timezone(timezone)
self.translations = {}
self.renderer = self._setup_renderer(self.templates_dir)
def _setup_renderer(self, templates_dir):
"""
Setup Jinja2 template renderer.
:param str templates_dir: Directory containing Jinja2 templates.
:return: Renderer as Jinja2 environment object.
:rtype: jinja2.Environment
"""
templates_dirs = self.templates_dir.split(";")
if len(templates_dirs) == 1:
renderer = jinja2.Environment(
loader = jinja2.FileSystemLoader(self.templates_dir),
extensions = ['jinja2.ext.i18n']
)
else:
renderer = jinja2.Environment(
loader = jinja2.ChoiceLoader([jinja2.FileSystemLoader(x) for x in templates_dirs]),
extensions = ['jinja2.ext.i18n']
)
#
# Ignore Pylint warnings, we need lambdas to create closures. Otherwise
# set_locale() does not have any effect.
renderer.install_gettext_callables(
lambda x: self.translator.gettext(x), # pylint: disable=locally-disabled,unnecessary-lambda
lambda s, p, n: self.translator.ngettext(s, p, n), # pylint: disable=locally-disabled,unnecessary-lambda
newstyle = True
)
renderer.globals['logger'] = self.logger
renderer.globals['get_locale'] = lambda: self.locale
renderer.globals['get_timezone'] = lambda: self.timezone
renderer.globals['get_timezone_local'] = lambda: self.local_tz_name
renderer.globals['format_decimal'] = self.format_decimal
renderer.globals['format_datetime'] = self.format_datetime
renderer.globals['format_localdatetime'] = self.format_localdatetime
renderer.globals['format_tzdatetime'] = self.format_tzdatetime
renderer.globals['format_rfctzdatetime'] = self.format_rfctzdatetime
renderer.globals['format_custom_datetime'] = self.format_custom_datetime
renderer.globals['format_timedelta'] = self.format_timedelta
renderer.globals['format_url'] = self.format_url
renderer.globals['sum_list'] = sum
renderer.globals['sum_dict'] = lambda x: sum(x.values())
renderer.globals['sort_keys_desc'] = lambda x: sorted(sorted(x.keys()), key = lambda y: x[y], reverse = True)
return renderer
#---------------------------------------------------------------------------
[docs] def set_locale(self, locale):
"""
Change internal locale to different value.
:param str locale: New locale as string.
"""
self.locale = locale
[docs] def set_timezone(self, timezone):
"""
Change internal timezone to different value.
:param str timezone: New timezone as string.
"""
self.timezone = timezone
if self.timezone != 'UTC':
self.tzinfo = get_timezone(self.timezone)
else:
self.tzinfo = UTC
@property
def translator(self):
"""
Return translator for currently active locale.
"""
if self.locale not in self.translations:
self.translations[self.locale] = gettext.NullTranslations()
for tdir in self.translations_dir.split(";"):
try:
translation = gettext.translation(
'messages',
tdir,
[self.locale]
)
translation.add_fallback(self.translations[self.locale])
self.translations[self.locale] = translation
except OSError:
continue
return self.translations[self.locale]
#---------------------------------------------------------------------------
[docs] def get_datetime(self, val):
"""
Method tries parse datetime if provided with string, otherwise return val directly.
"""
if isinstance(val, str):
try:
return dateutil.parser.parse(val)
except Exception:
pass
return val