Skip to content

Instantly share code, notes, and snippets.

@aliles
Last active January 3, 2016 05:39
Show Gist options
  • Save aliles/8417271 to your computer and use it in GitHub Desktop.
Save aliles/8417271 to your computer and use it in GitHub Desktop.
Technical proof-of-concept for using Jinja2 as a template language for API endpoints.
"""Proof of concept for adding external actions to Jinja2 templates
Demonstrates how control may be passed out of Jinja2 where additional
processing could occur and a value be passed back. Targetted use case is using
Jinja2 as a templating language for API endpoints.
"""
import os
import random
import string
import sys
import jinja2
import jinja2.ext
import requests
# Example of Jinja2 template code.
SOURCE = """{% set email = 'foo@foo.com' %}
{% set url = 'http://haveibeenpwned.com/api/breachedaccount/' + email|urlencode %}
{% http get url into result as json %}
{% for site in result %}
{{ site }}
{% endfor %}
"""
class HTTPExtension(jinja2.ext.Extension):
"""HTTP method extension for Jinja2 templates.
{% http METHOD URL into NAME as TYPE %}
Defers control out of template to the calling scope for exection of HTTP
request. A Mutable object is also passed out to allow external scope to
pass result back into template context.
"""
tags = set(['http'])
mutable = type('Mutable', (object,), {'__slots__': ('value',)})
def __init__(self, environment):
super(HTTPExtension, self).__init__(environment)
def parse(self, parser):
proxy = "_" + "".join(random.choice(string.letters) for i in range(16))
lineno = parser.stream.next().lineno
method = parser.stream.expect('name').value
url = parser.parse_expression()
_ = parser.stream.expect('name:into')
target = parser.stream.next().value
_ = parser.stream.expect('name:as')
content = parser.stream.expect('name').value
return [
# Creates new mutable proxy object inside template context.
jinja2.nodes.Assign(
jinja2.nodes.Name(proxy, 'store'),
jinja2.nodes.Call(
self.attr('mutable', lineno=lineno),
[], [], None, None)).set_lineno(lineno),
# Pass control out to calling scope.
jinja2.nodes.CallBlock(
self.call_method('action', [
jinja2.nodes.Name(proxy, 'load'),
jinja2.nodes.Const(method),
url,
jinja2.nodes.Const(content)
]), [], [], []).set_lineno(lineno),
# Retrives value and assigns it to final result target name.
jinja2.nodes.Assign(
jinja2.nodes.Name(target, 'store'),
jinja2.nodes.Getattr(
jinja2.nodes.Name(proxy, 'load'), 'value', 'load')
).set_lineno(lineno)
]
def action(self, proxy, method, url, content, caller):
def closure():
request = getattr(requests, method)
response = request(url)
data = getattr(response, content)
if callable(data):
data = data()
proxy.value = data
return closure
if __name__ == '__main__':
# Create Environment, with the HTTP extension, and load sample template
# code.
env = jinja2.Environment(trim_blocks=True, lstrip_blocks=True,
extensions=[HTTPExtension])
tmpl = env.from_string(SOURCE)
# Iteratively process the template in blocks using the template generator.
# If the generator returns a callable, call it. Otherwise, print the
# object to standard out.
for event in tmpl.generate():
if callable(event):
event()
else:
sys.stdout.write(event)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment