Skip to content
Snippets Groups Projects
redner.py 9.26 KiB
Newer Older
##############################################################################
#
#    Redner Odoo module
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
#    Copyright © 2016 XCG Consulting <https://xcg-consulting.fr>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

import logging
import time
Houzefa Abbasbhay's avatar
Houzefa Abbasbhay committed
import requests_unixsocket
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
from odoo import _
from odoo.exceptions import ValidationError

Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
_logger = logging.getLogger(__name__)
REDNER_API_PATH = "api/v1/"

Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
class Redner:
    def __init__(self, api_key, server_url, account, timeout):
        """Initialize the API client

        Args:
           api_key(str): provide your Redner API key.
           server_url(str): Redner server URL or socket path.
               For example: http://localhost:30001/
           timeout(float): Timeout per Redner call, in seconds.
        self.api_key = api_key
        self.account = account
        self.timeout = timeout

        if server_url.startswith("/"):
            self.session = requests_unixsocket.Session()
            self.server_url = "http+unix://{}/".format(quote(server_url, safe=""))
        else:
            self.session = requests.sessions.Session()
            self.server_url = server_url
        if not self.server_url.endswith("/"):
            self.server_url += "/"
        self.server_url += REDNER_API_PATH

        self.templates = Templates(self)

    def call(self, path, http_verb="post", **params):
        """Call redner with the specified parameters.
        Delegate to ``call_impl``; this is a wrapper to have some retries
        before giving up as redner sometimes mistakenly rejects our queries.
        """

        MAX_REDNERD_TRIES = 3
        for retry_counter in range(MAX_REDNERD_TRIES):
            try:
                return self.call_impl(path, http_verb=http_verb, **params)
            except Exception as error:
                if retry_counter == MAX_REDNERD_TRIES - 1:
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
                    _logger.error("Redner error: %s", str(error))
                    raise error

    def call_impl(self, path, http_verb="post", **params):
        """Actually make the API call with the given params -
        this should only be called by the namespace methods

        Args:
            path(str): URL path to query, eg. '/template/'
            http_verb(str): http verb to use, default: 'post'
            params(dict): json payload

        This method can raise anything; callers are expected to catch.
        """

        if not self.server_url:
            raise ValidationError(
                _(
                    "Cannot find redner config url. "
                    "Please add it in odoo.conf or in ir.config_parameter"
                )

        _http_verb = http_verb.upper()
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        _logger.info("Redner: Calling %s...", _http_verb)
        _logger.debug("Redner: Sending to %s > %s", url, params)
        start = time.time()

        r = getattr(self.session, http_verb, "post")(
            url,
            json=params,
            headers={"Rednerd-API-Key": self.api_key},
            timeout=self.timeout,
        )

        complete_time = time.time() - start
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        _logger.info(
            "Redner: Received %s in %.2fms.",
            r.status_code,
            complete_time * 1000,
        )
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        _logger.debug("Redner: Received %s", r.text)

        try:
            response = r.json()
        except Exception:
            # If we cannot decode JSON then it's an API error
            # having response as text could help debugging with sentry
            response = r.text

        if not str(r.status_code).startswith("2"):
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
            _logger.error("Bad response from Redner: %s", response)
            raise ValidationError(_("Unexpected redner error: %r") % response)

        return r.json()

    def ping(self):
        """Try to establish a connection to server"""
        conn = self.session.get(self.server_url, timeout=self.timeout)
        if conn.status_code != requests.codes.ok:
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
            raise ValidationError(_("Cannot Establish a connection to server"))
        return conn

    def __repr__(self):
        return "<Redner %s>" % self.api_key


Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
class Templates:
    def __init__(self, master):
        self.master = master

    def render(
        self,
        template_id,
        data,
        accept="text/html",
        body_format="base64",
        metadata=None,
    ):
        """Inject content and optionally merge fields into a template,
        returning the HTML that results.

        Args:
            template_id(str): Redner template ID.
            data(dict): Template variables.
            accept: format of a request or response body data.
            body_format (string): The body attribute format.
                Can be 'text' or 'base64'. Default 'base64',
            metadata (dict):

        Returns:
            Array of dictionaries: API response
        """

        if isinstance(data, dict):
            data = [data]

        params = {
            "accept": accept,
            "data": data,
            "template": {"account": self.master.account, "name": template_id},
            "body-format": body_format,
            "metadata": metadata or {},
        }
        return self.master.call("render", http_verb="post", **params)

    def account_template_add(
        self,
        language,
        body,
        name,
        produces="text/html",
        body_format="text",
        locale="fr_FR",
        version="N/A",
    ):
        """Store template in Redner

        Args:
            name(string): Name of your template. This is to help the user find
                its templates in a list.
            language(string): Language your template is written with.
                Can be mustache, handlebar or od+mustache.
            body(string): Content you want to create.

            produces(string): Can be text/html or

            body_format (string): The body attribute format. Can be 'text' or
                'base64'. Default 'base64'

            locale(string):

            version(string):

        Returns:
            name(string): Redner template Name.
        """

        params = {
            "name": name,
            "language": language,
            "body": body,
            "produces": produces,
            "body-format": body_format,
            "locale": locale,
            "version": version,
        }
        res = self.master.call(
            "template/%s" % self.master.account, http_verb="post", **params
        )
        return res["name"]

    def account_template_update(
        self,
        template_id,
        language,
        body,
        name="",
        produces="text/html",
        body_format="text",
        locale="fr_FR",
        version="N/A",
    ):
        """Store template in Redner

        Args:
            template_id(string): Name of your template.
            This is to help the user find its templates in a list.
            name(string): The new template name (optional)
            language(string): Language your template is written with.
                Can be mustache, handlebar or od+mustache

            body(string): Content you want to create.

            produces(string): Can be text/html or

            body_format (string): The body attribute format. Can be 'text' or
                'base64'. Default 'base64'

            locale(string):

            version(string):

        Returns:
            name(string): Redner template Name.
        """
        params = {
            "name": name,
            "language": language,
            "body": body,
            "produces": produces,
            "body-format": body_format,
            "locale": locale,
            "version": version,
        }
        res = self.master.call(
            "template/%s/%s" % (self.master.account, template_id),
            http_verb="put",
            **params,
        )
        return res["name"]

    def account_template_delete(self, name):
        """Delete a given template name

        Args:
            name(string): Redner template Name.

        Returns:
            dict: API response.
        """
        return self.master.call(
            "template/%s/%s" % (self.master.account, name), http_verb="delete"
        )

    def account_template_varlist(self, name):
        """Extract the list of variables present in the template.
        The list is not quaranteed to be accurate depending on the
        template language.

        Args:
            name(string): Redner template name.

        Returns:
            dict: API response.
        """

        params = {"account": self.master.account, "name": name}

        return self.master.call("varlist", **params)