Skip to content
Snippets Groups Projects
redner.py 7.76 KiB
Newer Older
import logging
import time

import requests

from odoo.exceptions import ValidationError

try:
    from urllib.parse import urljoin
except ImportError:
    from urlparse import urljoin


logger = logging.getLogger(__name__)


class Redner(object):
    def __init__(self, api_key, server_url, account):
        """Initialize the API client

        Args:
           api_key(str): provide your Redner API key.
           server_url(str): Redner server URL.
        """

        self.session = requests.session()
        self.account = account
        self.server_url = server_url
        self.api_key = api_key
        self.templates = Templates(self)

    def call(self, path, http_verb="post", **params):
        """Call redner with the specified paramters.
        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:
                    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"
            )

        url = urljoin(self.server_url, path)

        _http_verb = http_verb.upper()
        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}
        )

        complete_time = time.time() - start
        logger.info(
            "Redner: Received %s in %.2fms.",
            r.status_code,
            complete_time * 1000,
        )
        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 debuggin with sentry
            response = r.text

        if not str(r.status_code).startswith("2"):
            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)
        if conn.status_code != requests.codes.ok:
            raise ValidationError("Cannot Establish a connection to server")
        return conn

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


class Templates(object):
    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("v1/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(
            "v1/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(
            "v1/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(
            "v1/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("v1/varlist", **params)