diecutter

Templates as a service.

diecutter exposes an API where you manage templates as resources. The most common operation is to POST data to templates in order to retrieve generated files.

Files and directories are supported. Directories are rendered as archives.

Note

Diecutter is under active development: some (killer) features have not been implemented yet, or they are not mature. Check milestones and vision for details.

That said, features documented below actually work, so give it a try!

Example

GET raw content of a template:

$ curl -X GET http://diecutter.alwaysdata.net/api/greetings.txt
{{ greetings|default('Hello') }} {{ name }}!

POST data to the template and retrieve generated content:

$ curl -X POST -d name=world http://diecutter.alwaysdata.net/api/greetings.txt
Hello world!

More examples in Demo and Client howto!

Contents

Demo

Let’s try diecutter!

Online demo

There is an online server hosting diecutter’s demo:

In sourcecode

The demo/ directory in diecutter’s sourcecode [1] contains:

  • templates in templates/ folder.
  • some HTML-based client, specialized for the sphinx-docs template, as sphinx-docs.html file.
  • presets (input data) in presets/ folder.

Feel free to use it as a sandbox.

Note

the online demo uses templates from the source code!

Local demo server

System requirements:

  • Python [2] version 2.6 or 2.7, available as python command.

    Note

    You may use Virtualenv [3] to make sure the active python is the right one.

  • make and wget to use the provided Makefile.

Execute:

git clone git@github.com:novagile/diecutter.git
cd diecutter/
make develop
make serve

The last command runs diecutter service on localhost, port 8106. Check it at http://localhost:8106/

Tip

If you cannot execute the Makefile, read it and adapt the few commands it contains to your needs.

Examples

The following examples use the Local demo server. But you should be able to execute most of them on the online demo too.

The following examples are doctested [4], using Python’s WebTest [5] to run a diecutter server. Adapt the following setup to your needs:

>>> diecutter_url = diecutter_server.application_url

In the following examples, we use Python’s requests [6] as HTTP client.

>>> import requests
greetings.txt

Let’s work on file resource greetings.txt.

>>> greetings_url = diecutter_url + 'greetings.txt'

GET raw content of a template:

>>> response = requests.get(greetings_url)
>>> print response.content
{{ greetings|default('Hello') }} {{ name }}!

POST data to the template and retrieve generated content:

>>> response = requests.post(greetings_url, {'name': u'world'})
>>> print response.content
Hello world!

>>> response = requests.post(greetings_url, {'greetings': u'Greetings',
...                                          'name': u'professor Falken'})
>>> print response.content
Greetings professor Falken!

See Files for details about working with file resources.

django_admin.py startproject

Let’s work on directory resource +django_project+/.

>>> django_project_url = diecutter_url + '+django_project+'

It’s a directory. GET lists the templates it contains:

>>> response = requests.get(django_project_url)
>>> print response.content
+django_project+/manage.py
+django_project+/+django_project+/__init__.py
+django_project+/+django_project+/settings.py
+django_project+/+django_project+/urls.py
+django_project+/+django_project+/wsgi.py

Every template in a directory can be handled as a single file. But let’s focus on the directory feature...

POST returns an archive, with TAR.GZ format by default:

>>> django_project_url = diecutter_url + '+django_project+'

>>> from StringIO import StringIO
>>> import tarfile
>>> response = requests.post(django_project_url,
...                          {'django_project': u'demo'})
>>> response.headers['Content-Type']
'application/gzip; charset=UTF-8'
>>> archive = tarfile.open(fileobj=StringIO(response.content), mode='r|gz')
>>> print '\n'.join(archive.getnames())
demo/manage.py
demo/demo/__init__.py
demo/demo/settings.py
demo/demo/urls.py
demo/demo/wsgi.py
>>> archive.close()

Without trailing slash, directory name is used as a prefix for filenames. Let’s try the same requests with a trailing slash:

>>> django_project_url = diecutter_url + '+django_project+/'

>>> response = requests.get(django_project_url)
>>> print response.content
manage.py
+django_project+/__init__.py
+django_project+/settings.py
+django_project+/urls.py
+django_project+/wsgi.py

>>> response = requests.post(django_project_url,
...                          {'django_project': u'demo'})
>>> archive = tarfile.open(fileobj=StringIO(response.content), mode='r|gz')
>>> print '\n'.join(archive.getnames())
manage.py
demo/__init__.py
demo/settings.py
demo/urls.py
demo/wsgi.py
>>> archive.close()

You can get the content as a ZIP archive instead with the “accept” header:

>>> from zipfile import ZipFile
>>> response = requests.post(django_project_url,
...                          {'django_project': u'demo'},
...                          headers={'accept': 'application/zip'})
>>> print response.headers['Content-Type']
application/zip; charset=UTF-8
>>> archive = ZipFile(StringIO(response.content))
>>> print '\n'.join(archive.namelist())
manage.py
demo/__init__.py
demo/settings.py
demo/urls.py
demo/wsgi.py
>>> archive.close()

Tip

You can see all supported “accept” headers by requesting an unknown mime type:

>>> response = requests.post(django_project_url,
...                          {'django_project': u'demo'},
...                          headers={'accept': 'fake/mime-type'})
>>> response.status_code
406
>>> print response.content
406 Not Acceptable

The server could not comply with the request since it is either malformed or otherwise incorrect.


Supported mime types: */*, application/gzip, application/x-gzip, application/zip

See Directories for details about directory resources.

Dynamic tree template

Let’s work on directory resource dynamic-tree/.

>>> dynamic_tree_url = diecutter_url + 'dynamic-tree/'

It’s a directory. GET lists the templates it contains:

>>> response = requests.get(dynamic_tree_url)
>>> print response.content
.diecutter-tree
greeter.txt

greeter.txt is the same as greetings.txt example above:

>>> response = requests.get(dynamic_tree_url + 'greeter.txt')
>>> print response.content
{{ greeter|default('Hello') }} {{ name|default('world') }}!

diecutter-tree is a normal template, with a special name:

>>> response = requests.get(dynamic_tree_url + '.diecutter-tree')
>>> print response.content
[
  {% for greeter in greeting_list|default(['hello', 'goodbye']) %}
    {
      "template": "greeter.txt",
      "filename": "{{ greeter }}.txt",
      "context": {"greeter": "{{ greeter }}"}
    }{% if not loop.last %},{% endif %}
  {% endfor %}
]

It renders a list of templates, in JSON:

>>> response = requests.post(dynamic_tree_url + '.diecutter-tree',
...                          {'greeting_list': [u'bonjour', u'bonsoir']})
>>> print response.content
[

    {
      "template": "greeter.txt",
      "filename": "bonjour.txt",
      "context": {"greeter": "bonjour"}
    },

    {
      "template": "greeter.txt",
      "filename": "bonsoir.txt",
      "context": {"greeter": "bonsoir"}
    }

]

And guess what, this list of templates is used to render the directory resource:

>>> response = requests.post(dynamic_tree_url,
...                          {'name': u'Remy',
...                           'greeting_list': [u'bonjour', u'bonsoir']})
>>> archive = tarfile.open(fileobj=StringIO(response.content), mode='r:gz')
>>> print '\n'.join(archive.getnames())
bonjour.txt
bonsoir.txt
>>> print archive.extractfile('bonjour.txt').read()
bonjour Remy!
>>> print archive.extractfile('bonsoir.txt').read()
bonsoir Remy!
>>> archive.close()

Here, the greeter.txt template has been rendered several times, with different context data.

See Dynamic directory trees templates for details about dynamic tree templates.

References

[1]https://github.com/novagile/diecutter/
[2]http://python.org
[3]http://virtualenv.org
[4]http://sphinx-doc.org/ext/doctest.html
[5]https://pypi.python.org/pypi/WebTest
[6]https://pypi.python.org/pypi/requests

Client howto

This section focus on usage of diecutter service API, i.e. usage of diecutter from client’s point of view.

Files

Files are the main type of template resource.

Here is service’s API overview:

  • PUT: upload template file from client to server.
  • GET: retrieve raw template content.
  • POST: render single template file against context.
  • other HTTP verbs aren’t implemented yet.

Note

In the examples below, we’ll consider that a diecutter service is running at http://localhost:8106.

PUT

Put your template to the service:

$ echo "Hello {{ name }}" | curl -X PUT -F "file=@-" http://localhost:8106/hello.txt
{"diecutter": "Ok"}

Warning

Sometimes, you don’t want users to be able to PUT files on your server. That’s why diecutter service can be configured as “read only”. In that case, it is up to you to manage files that live in diecutter’s template directory (which is just a directory in the filesystem).

As an example, diecutter’s online demo is readonly, and templates are synchronized with source code on project’s repository.

GET

We can get the raw template we just created via PUT:

$ curl http://localhost:8106/hello.txt
Hello {{ name }}
POST

And we can render the template against some variables:

$ curl -X POST -d name=world http://localhost:8106/hello.txt
Hello world

Directories

Directories are the second type of template resource. They are collections of files and directories resources.

Here is service’s API overview:

  • PUT: directories are automatically created when you PUT a file in it. There is currently no way to create a whole directory with one request (uploading an archive as an example).
  • GET: list files in directory.
  • POST: render templates in directory against context, as an archive.
  • other HTTP verbs aren’t implemented yet.

Note

In the examples below, we’ll consider that a diecutter service is running at http://localhost:8106.

PUT

Let’s create some “greetings” directory with “hello.txt” and “goodbye.txt” inside:

$ echo "Hello {{ name }}!" | curl -X PUT  -F "file=@-" http://localhost:8106/greetings/hello.txt
{"diecutter": "Ok"}

$ echo "Good bye {{ name }}!" | curl -X PUT  -F "file=@-" http://localhost:8106/greetings/goodbye.txt
{"diecutter": "Ok"}
GET

GET lists files in directory:

$ curl http://localhost:8106/greetings/
hello.txt
goodbye.txt

Notice that, if you don’t set the trailing slash, you get the same list with folder name as prefix:

$ curl http://localhost:8106/greetings
greetings/hello.txt
greetings/goodbye.txt
POST

POST renders directory against a context:

$ curl -X POST -d name="world" -s http://localhost:8106/greetings | tar -zx

$ tree greetings
greetings
├── hello.txt
└── goodbye.txt
0 directory, 2 files

$ cat greetings/hello.txt
Hello world!

$ cat greetings/goodbye.txt
Goodbye world!

All files in directory are rendered with the same context data.

Like with GET, the trailing slash affects filenames: without trailing slash, filenames are prefixed with directory name.

Supported archives types: “accept” header

By default, POST requests return GZIP archives.

You can get the content as a TAR.GZ archive with the “accept” header:

$ curl -X POST --header "accept:application/zip" -d name="world" http://localhost:8106/greetings > greetings.zip
$ unzip greetings.zip
$ tree greetings
greetings
├── hello.txt
└── goodbye.txt
0 directory, 2 files

Tip

You can see all supported “accept” headers by requesting an unknown mime type:

$ curl -X POST --header "accept:fake/mime-type" -d name="world" http://localhost:8106/greetings
406 Not Acceptable

The server could not comply with the request since it is either malformed or
otherwise incorrect.


Supported mime types: */*, application/gzip, application/x-gzip,
application/zip

Posting input context data

When you perform POST requests on resources, you provide context data, i.e. variables and values.

Diecutter has builtin support for the following input content-types:

Diecutter expects data to be provided as the body of the request. “multipart/form-data” requests aren’t supported currently.

URL-encoded
# Default (implicit application/x-www-form-urlencoded content type).
curl -X POST -d 'who=world' http://localhost:8106/hello

# Explicit "application/x-www-form-urlencoded" content-type.
curl -X POST -d 'who=world' -H "Content-Type: application/x-www-form-urlencoded" http://localhost:8106/hello
JSON
curl -X POST -d '{"who": "world"}' -H "Content-Type: application/json" http://localhost:8106/hello
INI
# Flat.
curl -X POST -d 'who=world' -H "Content-Type: text/plain" http://localhost:8106/hello

# With sections.
cat > input.ini <<EOF
hello = world
[foo]
bar = baz
EOF
curl -X POST --data-binary '@input.ini' -H "Content-Type: text/plain" http://localhost:8106/foo
curl tips
  • Pass content of a file using @.
  • Pass content from standard input using @-.
  • When posting JSON or INI, use --data-binary.

References

[1]http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
[2]http://json.org/
[3]https://en.wikipedia.org/wiki/INI_file

Variable-based output filenames

When rendering a directory, it is sometimes useful to use variables in the filenames to output.

Note

This is useless for single files, since most clients allow you to choose the name of the output file.

Setup templates with +variable+ in the filename:

echo "Focus on filename" | curl -X PUT -F "file=@-" http://localhost:8106/foo/+bar+.txt

The variables within + will be resolved and replaced by their value:

$ curl -X POST -d bar=baz http://localhost:8106/foo | tar -zt
foo/baz.txt

Note

This behaviour is disabled when using the template tree template.

This feature is meant for simple cases. If you need advanced things for filenames, use the template tree template.

Dynamic directory trees templates

Directory trees can be computed from templates.

Thus you can use all features of template engines to render filenames, select templates or even alter context.

Use cases

While rendering a directory...

  • skip some files based on variables ;
  • render a single template several times with different output filenames ;
  • alter template context data for some templates ;
  • use loops, conditions... and all template-engine features...
The template tree template

When you POST to a directory, diecutter looks for special ”.diecutter-tree” template in that directory. If present, it renders ”.diecutter-tree” against context, decodes JSON, then iterates over items to actually render the directory.

Except when rendering a directory resource, ”.diecutter-tree” template is a normal template resource file. Manage it as any other template file.

Example

Let’s explain this feature with an example...

Create a “dynamic-greetings” directory resource, which contains only one “greetings.txt” template:

echo '{{ greeting }} {{ who }}!' | curl -X PUT -F "file=@-" http://localhost:8106/dynamic-greetings/greetings.txt

Let’s make “dynamic-greetings” directory dynamic, by putting a .diecutter-tree template:

echo '[
  {% for greeting in greetings %}
    {
      "template": "greetings.txt",
      "filename": "{{ greeting }}.txt",
      "context": {"greeting": "{{ greeting }}"}
    }{% if not loop.last %},{% endif %}
  {% endfor %}
]
' | curl -X PUT -F "file=@-" http://localhost:8106/dynamic-greetings/.diecutter-tree

POST to .diecutter-tree returns a JSON-encoded list:

curl -X POST -d greetings=hello -d greetings=goodbye http://localhost:8106/dynamic-greetings/.diecutter-tree
[
  {
    "template": "greetings.txt",
    "filename": "hello.txt",
    "context": {"greeting": "hello"}
  },
  {
    "template": "greetings.txt",
    "filename": "goodbye.txt",
    "context": {"greeting": "goodbye"}
  }
]

JSON-encoded list items are dictionaries with the following keys:

  • “template”: relative path to a template, i.e. content to be rendered ;
  • “filename”: filename to return to the client ;
  • “context”: optional dictionary of context overrides.

Now let’s render the directory as an archive:

$ curl -X POST \
       -d name=Remy -d greetings=hello -d greetings=goodbye \
       http://localhost:8106/dynamic-greetings \
       | tar -zxv
$ tree dynamic-greetings
dynamic-greetings
├── hello.txt
└── goodbye.txt
0 directory, 2 files

$ cat hello.txt
hello Remy!

$ cat goodbye.txt
goodbye Remy!

Server howto

This section explains how to setup, configure and run a diecutter server.

Install, configure, run

This project is open-source, published under BSD license. See License for details.

If you want to install a development environment, you should go to Contributor guide documentation.

Prerequisites
Install

Install “diecutter” package with your favorite tool. As an example with pip [2]:

pip install diecutter
Configure

Use diecutter’s online demo [3] to generate your local diecutter configuration:

# Adapt "YOUR_TEMPLATE_DIR"!
wget -O diecutter.ini --post-data "template_dir=YOUR_TEMPLATE_DIR" http://diecutter.alwaysdata.net/api/diecutter.ini
Run

pserve (paster’s server) should have been installed automatically as part of diecutter’s dependencies. Use it to run the service:

pserve diecutter.ini --reload
Check

Check it works:

curl http://localhost:8106

You should get an “hello” with diecutter’s version.

References

[1]http://python.org
[2]https://pypi.python.org/pypi/pip/
[3]http://diecutter.alwaysdata.net/

Framework

Diecutter is developped with extensibility in mind. Even if not stable yet, the idea is to build a default implementation upon a flexible framework. So that anyone can adapt the “templates as a service” pattern to his specific needs.

This section dives into diecutter’s internals and explains how to use or extend parts of it.

API

Here is API documentation, generated from code.

diecutter
diecutter Package
diecutter Package

Main entry point

diecutter.__init__.main(global_config, **settings)
contextextractors Module

Utilities to extract context dictionary from request.

diecutter.contextextractors.CONTEXT_EXTRACTORS = {'': <function extract_post_context at 0x31a1e60>, 'application/json': <function extract_json_context at 0x31a1f50>, 'text/plain': <function extract_ini_context at 0x319ac08>, 'application/x-www-form-urlencoded': <function extract_post_context at 0x31a1e60>}

Default context extractors configuration.

This configuration is used as fallback value if EXTRACTORS_SETTINGS is not in Pyramid’s registry.

This is a dictionary where:

  • keys are (lowercase) content-types.
  • values are callables which accept one request argument and return a dictionary (or dictionary-like object).
diecutter.contextextractors.EXTRACTORS_SETTING = 'diecutter.context_extractors'

Key in Pyramid registry where context extractors configuration lives.

diecutter.contextextractors.extract_context(request)

Extract context dictionary from request and return it.

Raise NotImplementedError if request input (content-type) is not supported.

diecutter.contextextractors.extract_ini_context(request)

Extract and return context from a text/ini (ConfigParser) request.

diecutter.contextextractors.extract_json_context(request)

Extract and return context from a application/json request.

diecutter.contextextractors.extract_post_context(request)

Extract and return context from a standard POST request.

diecutter.contextextractors.get_context_extractors(request)

Return context extractors configuration from request.

exceptions Module
exception diecutter.exceptions.DataParsingError

Bases: exceptions.Exception

Input data failed to be parsed.

exception diecutter.exceptions.TemplateError

Bases: exceptions.Exception

A template failed to be rendered.

resources Module

Resources exposed on APIs.

class diecutter.resources.DirResource(path='', engine=None, filename_engine=None)

Bases: diecutter.resources.Resource

Container for other files and directories resources.

content_type
exists
get_file_resource(path)

Factory for internal FileResources.

get_tree_template()

Return FileResource that holds directory tree.

has_tree_template()

Return True if .diecutter-tree file exists.

is_file = False
read()

Return directory tree as a list of paths of file resources.

read_tree()

Generate list of paths to contained resources.

relative_filename(filename)

Return filename relative to path.

>>> from diecutter.resources import DirResource
>>> resource = DirResource(path='abs/path/no-trailing')
>>> resource.relative_filename('abs/path/no-trailing/name')
'no-trailing/name'
>>> resource.relative_filename('abs/path/no-trailing/nested/name')
'no-trailing/nested/name'

Trailing slash in path affects returned value.

>>> resource = DirResource(path='abs/path/trailing/')
>>> resource.relative_filename('abs/path/trailing/name')
'name'
>>> resource.relative_filename('abs/path/trailing/nested/name')
'nested/name'
render(context)

Return archive of files in tree rendered against context.

render_file(template, context)

Render a file with context.

render_tree(context)

Generate list of (resource_path, filename, context).

Included resources may depend on context, i.e. some resources may be used several times, or skipped.

Rendered filenames may depend on context, i.e. variables may be used to render filenames.

Context may change for each resource.

render_tree_from_template(template, context)

Generate directory tree from a template file resource.

class diecutter.resources.FileResource(path='', engine=None, filename_engine=None)

Bases: diecutter.resources.Resource

content_type
exists
is_file = True
read()

Return the template source file.

render(context)

Return the template rendered against context.

class diecutter.resources.Resource(path='', engine=None, filename_engine=None)

Bases: object

content_type
exists
is_dir

Return True if resource is a collection of files.

is_file

Return True if resource is a single file.

read()

Return resource content.

render(context)

Return resource rendered against context.

render_filename(path, context)

Return rendered filename against context using FilenameEngine.

validators Module
diecutter.validators.token_validator(request)
views Module

Cornice services.

class diecutter.views.FirstResultDispatcher(runners=[])

Bases: object

A dispatcher that return the first result got from callables.

diecutter.views.get_accepted_types(request)

Return list of accepted content types from request’s ‘accept’ header.

diecutter.views.get_conf_template(request)
diecutter.views.get_dispatcher(request, resource, context, writers)

Return simple dispatcher (later, would read configuration).

diecutter.views.get_hello(request)

Returns Hello in JSON.

diecutter.views.get_resource(request)

Return the resource matching request.

Return value is a FileResource or :py:class`DirResource`.

diecutter.views.get_resource_path(request)

Return validated (absolute) resource path from request.

Checks that resource path is inside request’s template_dir.

diecutter.views.get_template_dir(request)

Return validated template directory configuration for request.

diecutter.views.get_writers(request, resource, context)

Return iterable of writers.

diecutter.views.is_readonly(request)

Return “readonly” flag status (boolean) for request.

As an example, PUT operations should be forbidden if readonly flag is On.

diecutter.views.post_conf_template(request)
diecutter.views.put_template(request)
diecutter.views.to_boolean(value)
writers Module

Writers: utilities that write template output as response, files...

diecutter.writers.file_response(request, resource, context)

Render file resource against context, return plain text response.

diecutter.writers.targz_directory(directory_content)

Return a tar.gz file built from iterable directory_content.

directory_content has the same format as diecutter.resources.DirResource.render() output.

diecutter.writers.targz_directory_response(request, resource, context)

Render dir resource against context, return result as tar.gz response.

diecutter.writers.zip_directory(directory_content)

Return a zip file built from iterable directory_contents.

directory_content has the same format as diecutter.resources.DirResource.render() output.

diecutter.writers.zip_directory_response(request, resource, context)

Render dir resource against context, return result as zip response.

Subpackages
engines Package
engines Package

Template engines.

class diecutter.engines.Engine

Bases: object

Base class for template engines.

Mostly used to document engine API.

Subclasses must implement render():

>>> from diecutter.engines import Engine
>>> engine = Engine()
>>> engine.render('fake-template', {'fake': 1})  # Doctest: +ELLIPSIS
Traceback (most recent call last):
  ...
NotImplementedError: Subclasses of "diecutter.engines.Engine" must implement render() method.
render(template, context)

Return the rendered template against context.

filename Module

Template engine specialized to render filenames.

class diecutter.engines.filename.FilenameEngine

Bases: diecutter.engines.Engine

render(template, context)

Return rendered filename template against context.

Warning

Only flat string variables are accepted. Other variables are ignored silently!

jinja Module

Jinja2 template engine.

class diecutter.engines.jinja.Jinja2Engine(environment=None)

Bases: diecutter.engines.Engine

Jinja2 template engine.

register_environment_functions()

Populate self.environment.globals with some global functions.

render(template, context)

Return the rendered template against context.

diecutter.engines.jinja.path_join(*args, **kwargs)

Return args joined as file paths like with os.path.join().

>>> from diecutter.engines.jinja import path_join
>>> path_join('foo', 'bar')
'foo/bar'

Paths are normalized.

>>> path_join('foo', '..', 'bar')
'bar'

You can pass an extra keyword argument ‘target_os’: a value in os.name capabilities.

>>> path_join('foo', 'bar', target_os='posix')
'foo/bar'

Currently, this is using os.path, i.e. the separator and rules for the computer running Jinja2 engine. A NotImplementedError exception will be raised if ‘os’ argument differs from ‘os.name’.

>>> import os
>>> os.name == 'posix'  # Sorry if you are running tests on another OS.
True
>>> path_join('foo', 'bar', target_os='nt')  # Doctest: +ELLIPSIS
Traceback (most recent call last):
  ...
NotImplementedError: Cannot join path with "nt" style. Host OS is "posix".
diecutter.engines.jinja.path_normalize(path, target_os=None)

Normalize path (like os.path.normpath) for given os.

>>> from diecutter.engines.jinja import path_normalize
>>> path_normalize('foo/bar')
'foo/bar'
>>> path_normalize('foo/toto/../bar')
'foo/bar'

Currently, this is using os.path, i.e. the separator and rules for the computer running Jinja2 engine. A NotImplementedError exception will be raised if ‘os’ argument differs from ‘os.name’.

>>> import os
>>> os.name == 'posix'  # Sorry if you are running tests on another OS.
True
>>> path_normalize('foo/bar', target_os='nt')  # Doctest: +ELLIPSIS
Traceback (most recent call last):
  ...
NotImplementedError: Cannot join path with "nt" style. Host OS is "posix".
mock Module

Mock template engine, for use in tests.

class diecutter.engines.mock.MockEngine(render_result=u'RENDER WITH ARGS={args!s} AND KWARGS={kwargs!s}', fail=None)

Bases: diecutter.engines.Engine

Template engine mock.

Typical usage:

>>> from diecutter.engines.mock import MockEngine
>>> mock_result = u'this is expected result'
>>> mock = MockEngine(mock_result)
>>> args = ('arg 1', 'arg 2')
>>> kwargs = {'kwarg1': 'kwarg 1', 'kwarg2': 'kwarg 2'}
>>> mock.render(*args, **kwargs) == mock_result
True
>>> mock.args == args
True
>>> mock.kwargs == kwargs
True

You can use {args} and {kwargs} in mock result, because render() uses self.render_result.format(args=args, kwargs=kwargs). This feature is used by default:

>>> mock = MockEngine()
>>> mock.render_result
u'RENDER WITH ARGS={args!s} AND KWARGS={kwargs!s}'
>>> mock.render()
u'RENDER WITH ARGS=() AND KWARGS={}'

If you setup an exception as fail attribute, then render() will raise that exception.

>>> mock = MockEngine(fail=Exception('An error occured'))
>>> mock.render()  # Doctest: +ELLIPSIS
Traceback (most recent call last):
...
Exception: An error occured
args = None

Stores positional arguments of the last call to render().

fail = None

Whether to raise a TemplateError or not. Also, value used as message in the exception.

kwargs = None

Stores keyword arguments of the last call to render().

render(*args, **kwargs)

Return self.render_result + populates args and kwargs.

If self.fail is not None, then raises a TemplateError(self.fail).

render_result = None

Value to be returned by render().

diecutter.engines.mock.default_render_result = u'RENDER WITH ARGS={args!s} AND KWARGS={kwargs!s}'

Default value used as MockEngine.render_result

tests Package
tests Package

Tests.

diecutter.tests.demo_template_dir()

Return absolute path to diecutter’s demo template dir.

diecutter.tests.settings(template_dir)

Shortcut to get diecutter settings for use in WSGI application.

class diecutter.tests.temporary_directory

Bases: object

Create, yield, and finally delete a temporary directory.

>>> from diecutter.tests import temporary_directory
>>> import os
>>> with temporary_directory() as directory:
...     os.path.isdir(directory)
True
>>> os.path.exists(directory)
False

Deletion of temporary directory is recursive.

>>> with temporary_directory() as directory:
...     filename = os.path.join(directory, 'sample.txt')
...     __ = open(filename, 'w').close()
...     os.path.isfile(filename)
True
>>> os.path.isfile(filename)
False
diecutter.tests.wsgi_application(settings={})

Return diecutter WSGI application for tests.

Uses WebTest.

diecutter.tests.wsgi_server(application)

Return (running) WebTest’s StopableWSGIServer for application.

contextextractors Module

Tests around diecutter.contextextractors.

class diecutter.tests.contextextractors.ExtractContextTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.contextextractors.extract_context().

extractor_factory(output)

Return callable that takes a request and returns output.

request_factory(extractors={}, content_type='')

Return request with extractors setting and content_type.

test_mapping()

extract_context() uses extractor matching content-type.

test_no_mapping()

extract_context() raises exception if content-type isn’t supported.

class diecutter.tests.contextextractors.GetContextExtractorsTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test contextextractors.get_context_extractors().

request_factory(settings={})

Return mock request instance.

test_custom_configuration()

get_context_extractors() reads request.registry.settings.

test_default_configuration()

get_context_extractors() with no settings returns default ones.

class diecutter.tests.contextextractors.IniTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.contextextractors.extract_ini_context.

request_factory(data={})

Return mock request instance.

test_config_parser()

extract_ini_context() accepts standard ConfigParser input.

test_empty()

extract_ini_context() with empty data returns empty context.

test_globals()

extract_ini_context() accepts input with no sections.

test_parsing_error()

extract_ini_context() raises DataParsingError in case of error.

class diecutter.tests.contextextractors.JsonTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.contextextractors.extract_json_context.

request_factory(data={})

Return mock request instance.

test_copy()

extract_post_request() returns copy of request’s json_body.

test_data()

extract_post_request() returns request.json_body data.

class diecutter.tests.contextextractors.PostTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.contextextractors.extract_post_context.

request_factory(data={})

Return mock request instance.

test_copy()

extract_post_request() returns copy of request’s data.

test_data()

extract_post_request() returns request.post data.

test_multiple_values()

extract_post_request() handles lists.

functional Module

Functional tests: run test server and make requests on it.

class diecutter.tests.functional.FunctionalTestCase(methodName='runTest')

Bases: unittest.case.TestCase

setUp()

Run test server with temporary settings.

tearDown()

Cleanup temporary template dir.

test_get_directory()

GET a directory resource returns directory listing.

test_get_file()

GET a file resource returns file contents.

test_get_file_404()

GET a file resource that doesn’t exist returns HTTP 404.

test_post_directory_targz()

POST context for directory returns TAR.GZ file content.

test_post_directory_zip()

POST context for directory with accept header returns ZIP file.

test_post_file()

POST context for template returns rendered content.

test_put_file()

PUT a file as attachment writes file in templates directory.

test_put_file_subdirs()

PUT a file in subdirectories creates those directories.

test_version()

GET on root displays “hello” and version number as JSON.

resources Module

Tests around diecutter.resources.

class diecutter.tests.resources.DirResourceTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.resources.DirResource.

test_content_type()

DirResource.content_type is ‘application/zip’.

test_exists_dir()

DirResource.exists is True if path points a directory.

test_exists_false()

DirResource.exists is False if dir doesn’t exist at path.

test_exists_file()

DirResource.exists is False if path points a file.

test_has_tree_template()

DirResource.has_tree_template() checks if .diecutter-tree exists.

test_no_trailing_slash()

DirResource with no trailing slash uses dirname as prefix.

test_read_empty()

DirResource.read() empty dir returns empty string.

test_read_nested()

DirResource.read() recurses nested files and directories.

test_read_one_flat()

DirResource.read() one file returns one filename.

test_read_two_flat()

DirResource.read() two files returns two filenames.

test_render()

DirResource.render() returns an iterable of rendered templates.

test_render_dynamic_tree()

DirResource.render_tree() uses .diecutter-tree template.

test_render_dynamic_tree_relative_paths()

Raises exception if .diecutter-tree contains some non relative path.

Warning

This is a security test!

Since dynamic tree templates can be defined by user, we have to check templates path. We don’t want users to be able to render arbitrary locations on the filesystem.

test_render_template_error()
test_render_tree()

DirResource.render_tree() recurses nested files and directories.

test_trailing_slash()

DirResource with trailing slash uses dirname as prefix.

class diecutter.tests.resources.FileResourceTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.resources.FileResource.

test_content_type()

FileResource.content_type is ‘text/plain’.

test_exists_dir()

FileResource.exists is False if path points a directory.

test_exists_false()

FileResource.exists is False if file doesn’t exist at path.

test_exists_file()

FileResource.exists is True if path points a file.

test_read_empty()

FileResource.read() empty file returns empty string.

test_read_utf8()

FileResource.read() decodes UTF-8 files.

test_render()

FileResource.render() generates rendered template against context.

It returns an iteratable or generator.

test_render_error()

FileResource.render() raises TemplateError in case of fail.

class diecutter.tests.resources.ResourceTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.resources.Resource.

test_content_type()

Resource.content_type property must be overriden by subclasses.

test_exists()

Resource.exists property must be overriden by subclasses.

test_init()

Resource constructor accepts optional path and engine.

test_read()

Resource.read() must be overriden by subclasses.

test_render()

Resource.render() must be overriden by subclasses.

test_render_filename()

Resource.render_filename() renders filename against context.

Subpackages
engines Package
engines Package

Tests around diecutter.engines package.

filename Module

Tests around diecutter.engines.filename.

class diecutter.tests.engines.filename.FilenameTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.engines.filename.FilenameEngine.

test_render()

FilenameEngine.render() renders filename against context.

test_render_error()

FilenameEngine.render() only accepts flat string variables.

Warning

Only flat string variables are accepted. Other variables are ignored silently!

jinja Module

Tests around diecutter.engines.jinja.

class diecutter.tests.engines.jinja.Jinja2TestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.engines.jinja.Jinja2Engine.

test_environment()

Jinja2Engine’s environment contains additional functions.

test_render_noop()

Jinja2Engine correctly renders Hello world! template.

test_render_simple()

Jinja2Engine correctly renders Hello {{ name }}! template.

test_template_error()

Jinja2Engine raises TemplateError in case of exception.

diecutter
diecutter Package
diecutter Package

Main entry point

diecutter.__init__.main(global_config, **settings)
contextextractors Module

Utilities to extract context dictionary from request.

diecutter.contextextractors.CONTEXT_EXTRACTORS = {'': <function extract_post_context at 0x31a1e60>, 'application/json': <function extract_json_context at 0x31a1f50>, 'text/plain': <function extract_ini_context at 0x319ac08>, 'application/x-www-form-urlencoded': <function extract_post_context at 0x31a1e60>}

Default context extractors configuration.

This configuration is used as fallback value if EXTRACTORS_SETTINGS is not in Pyramid’s registry.

This is a dictionary where:

  • keys are (lowercase) content-types.
  • values are callables which accept one request argument and return a dictionary (or dictionary-like object).
diecutter.contextextractors.EXTRACTORS_SETTING = 'diecutter.context_extractors'

Key in Pyramid registry where context extractors configuration lives.

diecutter.contextextractors.extract_context(request)

Extract context dictionary from request and return it.

Raise NotImplementedError if request input (content-type) is not supported.

diecutter.contextextractors.extract_ini_context(request)

Extract and return context from a text/ini (ConfigParser) request.

diecutter.contextextractors.extract_json_context(request)

Extract and return context from a application/json request.

diecutter.contextextractors.extract_post_context(request)

Extract and return context from a standard POST request.

diecutter.contextextractors.get_context_extractors(request)

Return context extractors configuration from request.

exceptions Module
exception diecutter.exceptions.DataParsingError

Bases: exceptions.Exception

Input data failed to be parsed.

exception diecutter.exceptions.TemplateError

Bases: exceptions.Exception

A template failed to be rendered.

resources Module

Resources exposed on APIs.

class diecutter.resources.DirResource(path='', engine=None, filename_engine=None)

Bases: diecutter.resources.Resource

Container for other files and directories resources.

content_type
exists
get_file_resource(path)

Factory for internal FileResources.

get_tree_template()

Return FileResource that holds directory tree.

has_tree_template()

Return True if .diecutter-tree file exists.

is_file = False
read()

Return directory tree as a list of paths of file resources.

read_tree()

Generate list of paths to contained resources.

relative_filename(filename)

Return filename relative to path.

>>> from diecutter.resources import DirResource
>>> resource = DirResource(path='abs/path/no-trailing')
>>> resource.relative_filename('abs/path/no-trailing/name')
'no-trailing/name'
>>> resource.relative_filename('abs/path/no-trailing/nested/name')
'no-trailing/nested/name'

Trailing slash in path affects returned value.

>>> resource = DirResource(path='abs/path/trailing/')
>>> resource.relative_filename('abs/path/trailing/name')
'name'
>>> resource.relative_filename('abs/path/trailing/nested/name')
'nested/name'
render(context)

Return archive of files in tree rendered against context.

render_file(template, context)

Render a file with context.

render_tree(context)

Generate list of (resource_path, filename, context).

Included resources may depend on context, i.e. some resources may be used several times, or skipped.

Rendered filenames may depend on context, i.e. variables may be used to render filenames.

Context may change for each resource.

render_tree_from_template(template, context)

Generate directory tree from a template file resource.

class diecutter.resources.FileResource(path='', engine=None, filename_engine=None)

Bases: diecutter.resources.Resource

content_type
exists
is_file = True
read()

Return the template source file.

render(context)

Return the template rendered against context.

class diecutter.resources.Resource(path='', engine=None, filename_engine=None)

Bases: object

content_type
exists
is_dir

Return True if resource is a collection of files.

is_file

Return True if resource is a single file.

read()

Return resource content.

render(context)

Return resource rendered against context.

render_filename(path, context)

Return rendered filename against context using FilenameEngine.

validators Module
diecutter.validators.token_validator(request)
views Module

Cornice services.

class diecutter.views.FirstResultDispatcher(runners=[])

Bases: object

A dispatcher that return the first result got from callables.

diecutter.views.get_accepted_types(request)

Return list of accepted content types from request’s ‘accept’ header.

diecutter.views.get_conf_template(request)
diecutter.views.get_dispatcher(request, resource, context, writers)

Return simple dispatcher (later, would read configuration).

diecutter.views.get_hello(request)

Returns Hello in JSON.

diecutter.views.get_resource(request)

Return the resource matching request.

Return value is a FileResource or :py:class`DirResource`.

diecutter.views.get_resource_path(request)

Return validated (absolute) resource path from request.

Checks that resource path is inside request’s template_dir.

diecutter.views.get_template_dir(request)

Return validated template directory configuration for request.

diecutter.views.get_writers(request, resource, context)

Return iterable of writers.

diecutter.views.is_readonly(request)

Return “readonly” flag status (boolean) for request.

As an example, PUT operations should be forbidden if readonly flag is On.

diecutter.views.post_conf_template(request)
diecutter.views.put_template(request)
diecutter.views.to_boolean(value)
writers Module

Writers: utilities that write template output as response, files...

diecutter.writers.file_response(request, resource, context)

Render file resource against context, return plain text response.

diecutter.writers.targz_directory(directory_content)

Return a tar.gz file built from iterable directory_content.

directory_content has the same format as diecutter.resources.DirResource.render() output.

diecutter.writers.targz_directory_response(request, resource, context)

Render dir resource against context, return result as tar.gz response.

diecutter.writers.zip_directory(directory_content)

Return a zip file built from iterable directory_contents.

directory_content has the same format as diecutter.resources.DirResource.render() output.

diecutter.writers.zip_directory_response(request, resource, context)

Render dir resource against context, return result as zip response.

Subpackages
engines Package
engines Package

Template engines.

class diecutter.engines.Engine

Bases: object

Base class for template engines.

Mostly used to document engine API.

Subclasses must implement render():

>>> from diecutter.engines import Engine
>>> engine = Engine()
>>> engine.render('fake-template', {'fake': 1})  # Doctest: +ELLIPSIS
Traceback (most recent call last):
  ...
NotImplementedError: Subclasses of "diecutter.engines.Engine" must implement render() method.
render(template, context)

Return the rendered template against context.

filename Module

Template engine specialized to render filenames.

class diecutter.engines.filename.FilenameEngine

Bases: diecutter.engines.Engine

render(template, context)

Return rendered filename template against context.

Warning

Only flat string variables are accepted. Other variables are ignored silently!

jinja Module

Jinja2 template engine.

class diecutter.engines.jinja.Jinja2Engine(environment=None)

Bases: diecutter.engines.Engine

Jinja2 template engine.

register_environment_functions()

Populate self.environment.globals with some global functions.

render(template, context)

Return the rendered template against context.

diecutter.engines.jinja.path_join(*args, **kwargs)

Return args joined as file paths like with os.path.join().

>>> from diecutter.engines.jinja import path_join
>>> path_join('foo', 'bar')
'foo/bar'

Paths are normalized.

>>> path_join('foo', '..', 'bar')
'bar'

You can pass an extra keyword argument ‘target_os’: a value in os.name capabilities.

>>> path_join('foo', 'bar', target_os='posix')
'foo/bar'

Currently, this is using os.path, i.e. the separator and rules for the computer running Jinja2 engine. A NotImplementedError exception will be raised if ‘os’ argument differs from ‘os.name’.

>>> import os
>>> os.name == 'posix'  # Sorry if you are running tests on another OS.
True
>>> path_join('foo', 'bar', target_os='nt')  # Doctest: +ELLIPSIS
Traceback (most recent call last):
  ...
NotImplementedError: Cannot join path with "nt" style. Host OS is "posix".
diecutter.engines.jinja.path_normalize(path, target_os=None)

Normalize path (like os.path.normpath) for given os.

>>> from diecutter.engines.jinja import path_normalize
>>> path_normalize('foo/bar')
'foo/bar'
>>> path_normalize('foo/toto/../bar')
'foo/bar'

Currently, this is using os.path, i.e. the separator and rules for the computer running Jinja2 engine. A NotImplementedError exception will be raised if ‘os’ argument differs from ‘os.name’.

>>> import os
>>> os.name == 'posix'  # Sorry if you are running tests on another OS.
True
>>> path_normalize('foo/bar', target_os='nt')  # Doctest: +ELLIPSIS
Traceback (most recent call last):
  ...
NotImplementedError: Cannot join path with "nt" style. Host OS is "posix".
mock Module

Mock template engine, for use in tests.

class diecutter.engines.mock.MockEngine(render_result=u'RENDER WITH ARGS={args!s} AND KWARGS={kwargs!s}', fail=None)

Bases: diecutter.engines.Engine

Template engine mock.

Typical usage:

>>> from diecutter.engines.mock import MockEngine
>>> mock_result = u'this is expected result'
>>> mock = MockEngine(mock_result)
>>> args = ('arg 1', 'arg 2')
>>> kwargs = {'kwarg1': 'kwarg 1', 'kwarg2': 'kwarg 2'}
>>> mock.render(*args, **kwargs) == mock_result
True
>>> mock.args == args
True
>>> mock.kwargs == kwargs
True

You can use {args} and {kwargs} in mock result, because render() uses self.render_result.format(args=args, kwargs=kwargs). This feature is used by default:

>>> mock = MockEngine()
>>> mock.render_result
u'RENDER WITH ARGS={args!s} AND KWARGS={kwargs!s}'
>>> mock.render()
u'RENDER WITH ARGS=() AND KWARGS={}'

If you setup an exception as fail attribute, then render() will raise that exception.

>>> mock = MockEngine(fail=Exception('An error occured'))
>>> mock.render()  # Doctest: +ELLIPSIS
Traceback (most recent call last):
...
Exception: An error occured
args = None

Stores positional arguments of the last call to render().

fail = None

Whether to raise a TemplateError or not. Also, value used as message in the exception.

kwargs = None

Stores keyword arguments of the last call to render().

render(*args, **kwargs)

Return self.render_result + populates args and kwargs.

If self.fail is not None, then raises a TemplateError(self.fail).

render_result = None

Value to be returned by render().

diecutter.engines.mock.default_render_result = u'RENDER WITH ARGS={args!s} AND KWARGS={kwargs!s}'

Default value used as MockEngine.render_result

tests Package
tests Package

Tests.

diecutter.tests.demo_template_dir()

Return absolute path to diecutter’s demo template dir.

diecutter.tests.settings(template_dir)

Shortcut to get diecutter settings for use in WSGI application.

class diecutter.tests.temporary_directory

Bases: object

Create, yield, and finally delete a temporary directory.

>>> from diecutter.tests import temporary_directory
>>> import os
>>> with temporary_directory() as directory:
...     os.path.isdir(directory)
True
>>> os.path.exists(directory)
False

Deletion of temporary directory is recursive.

>>> with temporary_directory() as directory:
...     filename = os.path.join(directory, 'sample.txt')
...     __ = open(filename, 'w').close()
...     os.path.isfile(filename)
True
>>> os.path.isfile(filename)
False
diecutter.tests.wsgi_application(settings={})

Return diecutter WSGI application for tests.

Uses WebTest.

diecutter.tests.wsgi_server(application)

Return (running) WebTest’s StopableWSGIServer for application.

contextextractors Module

Tests around diecutter.contextextractors.

class diecutter.tests.contextextractors.ExtractContextTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.contextextractors.extract_context().

extractor_factory(output)

Return callable that takes a request and returns output.

request_factory(extractors={}, content_type='')

Return request with extractors setting and content_type.

test_mapping()

extract_context() uses extractor matching content-type.

test_no_mapping()

extract_context() raises exception if content-type isn’t supported.

class diecutter.tests.contextextractors.GetContextExtractorsTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test contextextractors.get_context_extractors().

request_factory(settings={})

Return mock request instance.

test_custom_configuration()

get_context_extractors() reads request.registry.settings.

test_default_configuration()

get_context_extractors() with no settings returns default ones.

class diecutter.tests.contextextractors.IniTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.contextextractors.extract_ini_context.

request_factory(data={})

Return mock request instance.

test_config_parser()

extract_ini_context() accepts standard ConfigParser input.

test_empty()

extract_ini_context() with empty data returns empty context.

test_globals()

extract_ini_context() accepts input with no sections.

test_parsing_error()

extract_ini_context() raises DataParsingError in case of error.

class diecutter.tests.contextextractors.JsonTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.contextextractors.extract_json_context.

request_factory(data={})

Return mock request instance.

test_copy()

extract_post_request() returns copy of request’s json_body.

test_data()

extract_post_request() returns request.json_body data.

class diecutter.tests.contextextractors.PostTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.contextextractors.extract_post_context.

request_factory(data={})

Return mock request instance.

test_copy()

extract_post_request() returns copy of request’s data.

test_data()

extract_post_request() returns request.post data.

test_multiple_values()

extract_post_request() handles lists.

functional Module

Functional tests: run test server and make requests on it.

class diecutter.tests.functional.FunctionalTestCase(methodName='runTest')

Bases: unittest.case.TestCase

setUp()

Run test server with temporary settings.

tearDown()

Cleanup temporary template dir.

test_get_directory()

GET a directory resource returns directory listing.

test_get_file()

GET a file resource returns file contents.

test_get_file_404()

GET a file resource that doesn’t exist returns HTTP 404.

test_post_directory_targz()

POST context for directory returns TAR.GZ file content.

test_post_directory_zip()

POST context for directory with accept header returns ZIP file.

test_post_file()

POST context for template returns rendered content.

test_put_file()

PUT a file as attachment writes file in templates directory.

test_put_file_subdirs()

PUT a file in subdirectories creates those directories.

test_version()

GET on root displays “hello” and version number as JSON.

resources Module

Tests around diecutter.resources.

class diecutter.tests.resources.DirResourceTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.resources.DirResource.

test_content_type()

DirResource.content_type is ‘application/zip’.

test_exists_dir()

DirResource.exists is True if path points a directory.

test_exists_false()

DirResource.exists is False if dir doesn’t exist at path.

test_exists_file()

DirResource.exists is False if path points a file.

test_has_tree_template()

DirResource.has_tree_template() checks if .diecutter-tree exists.

test_no_trailing_slash()

DirResource with no trailing slash uses dirname as prefix.

test_read_empty()

DirResource.read() empty dir returns empty string.

test_read_nested()

DirResource.read() recurses nested files and directories.

test_read_one_flat()

DirResource.read() one file returns one filename.

test_read_two_flat()

DirResource.read() two files returns two filenames.

test_render()

DirResource.render() returns an iterable of rendered templates.

test_render_dynamic_tree()

DirResource.render_tree() uses .diecutter-tree template.

test_render_dynamic_tree_relative_paths()

Raises exception if .diecutter-tree contains some non relative path.

Warning

This is a security test!

Since dynamic tree templates can be defined by user, we have to check templates path. We don’t want users to be able to render arbitrary locations on the filesystem.

test_render_template_error()
test_render_tree()

DirResource.render_tree() recurses nested files and directories.

test_trailing_slash()

DirResource with trailing slash uses dirname as prefix.

class diecutter.tests.resources.FileResourceTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.resources.FileResource.

test_content_type()

FileResource.content_type is ‘text/plain’.

test_exists_dir()

FileResource.exists is False if path points a directory.

test_exists_false()

FileResource.exists is False if file doesn’t exist at path.

test_exists_file()

FileResource.exists is True if path points a file.

test_read_empty()

FileResource.read() empty file returns empty string.

test_read_utf8()

FileResource.read() decodes UTF-8 files.

test_render()

FileResource.render() generates rendered template against context.

It returns an iteratable or generator.

test_render_error()

FileResource.render() raises TemplateError in case of fail.

class diecutter.tests.resources.ResourceTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.resources.Resource.

test_content_type()

Resource.content_type property must be overriden by subclasses.

test_exists()

Resource.exists property must be overriden by subclasses.

test_init()

Resource constructor accepts optional path and engine.

test_read()

Resource.read() must be overriden by subclasses.

test_render()

Resource.render() must be overriden by subclasses.

test_render_filename()

Resource.render_filename() renders filename against context.

Subpackages
engines Package
engines Package

Tests around diecutter.engines package.

filename Module

Tests around diecutter.engines.filename.

class diecutter.tests.engines.filename.FilenameTestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.engines.filename.FilenameEngine.

test_render()

FilenameEngine.render() renders filename against context.

test_render_error()

FilenameEngine.render() only accepts flat string variables.

Warning

Only flat string variables are accepted. Other variables are ignored silently!

jinja Module

Tests around diecutter.engines.jinja.

class diecutter.tests.engines.jinja.Jinja2TestCase(methodName='runTest')

Bases: unittest.case.TestCase

Test diecutter.engines.jinja.Jinja2Engine.

test_environment()

Jinja2Engine’s environment contains additional functions.

test_render_noop()

Jinja2Engine correctly renders Hello world! template.

test_render_simple()

Jinja2Engine correctly renders Hello {{ name }}! template.

test_template_error()

Jinja2Engine raises TemplateError in case of exception.

Contributor guide

This document provides guidelines for people who want to contribute to the project.

Create tickets

Please use the bugtracker [1] before starting some work:

  • check if the bug or feature request has already been filed. It may have been answered too!
  • else create a new ticket.
  • if you plan to contribute, tell us, so that we are given an opportunity to give feedback as soon as possible.
  • Then, in your commit messages, reference the ticket with some refs #TICKET-ID syntax.

Fork and branch

Work in forks and branches.

  • Prefix your branch with the ticket ID corresponding to the issue. As an example, if you are working on ticket #23 which is about contribute documentation, name your branch like 23-contribute-doc.
  • If you work in a development branch and want to refresh it with changes from master, please rebase [2] or merge-based rebase [3], i.e. don’t merge master.

Setup a development environment

System requirements:

  • Python [4] version 2.7, available as python command.

    Note

    You may use Virtualenv [5] to make sure the active python is the right one.

  • make and wget to use the provided Makefile.

Execute:

git clone git@github.com:novagile/diecutter.git
cd diecutter/
make develop

If you cannot execute the Makefile, read it and adapt the few commands it contains to your needs.

The Makefile

A Makefile is provided to ease development.

Use it to run common development tasks, such as:

  • setup the development environment: make develop ;
  • run tests: make test ;
  • build documentation: make documentation...

The Makefile is intended to be a live reference for the development environment.

Demo project included

The Demo is part of the project. Maintain it along with code and documentation.

About diecutter

This section is about the diecutter project itself.

Vision

Diecutter is about file generation. Its primary goal is to provide an easy way to render templates against data.

Some leitmotivs:

  • focus on file generation.
  • in most use cases, don’t bother users with software installation and configuration.
  • when users have specific needs, they can easily setup a custom service.
API

API design drives diecutter’s development.

Server software

Diecutter provides a default implementation of the service, built with Python.

Framework

Diecutter is a framework. It must provide material that makes it easy to connect to other tools and easy to extend.

SAAS platform

Diecutter is developed with SAAS in mind. The online demo is a draft.

License

Copyright (c) 2012, Rémy Hubscher, Benoît Bryon. See Authors and contributors. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  • Neither the name of the diecutter software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Authors and contributors

Changelog

This document describes changes between each past release. For information about future releases, check milestones [1] and Vision.

0.4 (2013-07-17)

New feature (tar.gz archives) and marketing (talks).

  • Feature #4 - Added support of “accept” header for POST requests on directories: accepted types are ZIP (application/zip) and TAR.GZ (application/gzip).
  • Feature #53 - GZIP is now the default archive format when rendering directories. Use “diecutter.default_archive_type = application/zip” in configuration file if you need ZIP format as a default.
  • Refactoring #55 - Dropped support of Python 2.6. Tests are run against Python 2.7 only.
  • Refactoring #20 - Render functions return generator ; moved response composition (file/archive) into views via writers.
  • Feature #46 - Added content of talks in documentation: AFPY event and EuroPython 2013.
  • Feature #58 - Highlighted roadmap and vision in README.

See also milestone 0.4 on bugtracker [2].

0.3 (2013-04-16)

New features, documentation, bugfixes.

  • Bug #44 - Accepted arrays in URL-encoded POST.
  • Bug #40 - Setup CORS to allow AJAX requests on diecutter’s API.
  • Refactoring #37 - Used Jinja’s environment.
  • Bug #34 - Frozen buildout configuration file for development environment.
  • Features #31 and #43 - Published diecutter’s demo online. Online API URL changed.
  • Feature #24 - Added Sphinx documentation template in diecutter’s demo.
  • Feature #23 - Added diecutter’s Sphinx documentation.
  • Feature #10 - Added dynamic tree template.

See also milestone 0.3 on bugtracker [3].

0.2 (2013-02-22)

Maintenance release, implementation refactoring, tests.

  • Refactoring #22 - Added tests.
  • Bug #17 - Sort directories alphabetically.
  • Bug #13 - Fixed “diecutter.readonly” which was always True.

See also milestone 0.2 on bugtracker [4].

0.1 (2013-01-29)

Initial release.

  • Bug #11 - On POST requests, handle empty content-type as “application/x-www-form-urlencoded”.
  • Feature #8 - Support INI files as input for POST requests.
  • Feature #3 - Use a configuration file outside diecutter’s code.
  • Feature #2 - If “readonly” option is True, forbid PUT requests.
  • Feature #1 - Pass a “diecutter” context variable to templates, containing data such as “diecutter.api_url”, “diecutter.version” and “diecutter.now”.
  • Feature - Diecutter service renders directories as ZIP archives.
  • Feature - Diecutter service renders files.

See also milestone 0.1 on bugtracker [5].

Notes & references

[1]https://github.com/novagile/diecutter/issues/milestones
[2]https://github.com/novagile/diecutter/issues?milestone=7&state=closed
[3]https://github.com/novagile/diecutter/issues?milestone=6&state=closed
[4]https://github.com/novagile/diecutter/issues?milestone=2&state=closed
[5]https://github.com/novagile/diecutter/issues?milestone=1&state=closed

Presentations

Here are some presentations (mostly slides) about diecutter.

Diecutter: templates as a service

Presentation at an AFPY event, march 2013, Paris.

By Rémy Hubscher and Benoit Bryon.

Slides available on http://diecutter.readthedocs.org

This presentation is published under CC BY 3.0


Render templates
Client posts DATA to TEMPLATE which returns FILE

Render directories
Client posts DATA to DIRECTORY which returns ARCHIVE

Generate configuration

Setup template “diecutter.ini” server-side:

diecutter.template_dir = {{ template_dir|default('') }}
diecutter.token = {{ token|default('') }}
diecutter.readonly = {{ readonly|default('false') }}

Generate configuration client-side:

wget --post-data="template_dir=src/templates"  \
     --output-document="diecutter.ini"     \
     http://diecutter.alwaysdata.net/diecutter.ini

Generate code

django startproject reloaded:

curl --data "django_project=demo"                      \
     http://diecutter.alwaysdata.net/+django_project+  \
     > demo.zip

PasteScript is complicated
  • needs PasteScript and templates installed locally
  • template registration is a pain:
    • create template files (Cheetah markup)
    • create template class (Cheetah Template)
    • update template package’s entry points (setup.py)
    • update your environment (reinstall/update)
  • Cheetah?

Provisioners are overkill
  • I just want to render a template against data!
  • I don’t need Chef, Salt, Puppet & co.
  • Templates aren’t only useful in provisioning.

And I’d like to reuse templates whatever the provisioning tool!


Diecutter’s REST API
  • POST data and retrieve generated files
  • GET raw content of templates
  • PUT templates from client to server
  • DELETE template (1)
  • OPTIONS lists template variables (1)
  • PATCH alters default values of variables (1)

(1) Work in progress.


KISS
  • focus on API
  • no builtin client
  • no builtin form validation (have a look at daybed)

Encapsulated implementation(s)
  • template resources backend
    • Today: local filesystem.
    • Tomorrow: remote Git/Hg/..., NoSQL, include/extend URLs
  • template engines
    • Today: Jinja.
    • Tomorrow: Cheetah, ERB, XSLT...
  • response writers
    • Today: return single file or ZIP archive.
    • Tomorrow: tar.gz, write to storage, attachments, streaming, asynchronous generation...

Roadmap
  • today: proof of concept, focus on API
  • tomorrow: rock-solid defaults, focus on stability & performance
  • later: make your own, focus on extensibility

diecutter: templates as a service

This is a draft for diecutter poster session at EuroPython 2013, Florence.

Here is the poster: poster.pdf.

Render files & directories
Generate configuration
curl -X POST \
     -d ssl=on \
     -d domain=example.com \
     https://diecutter/nginx/ \
     | tar -zxv --directory=/etc/nginx/
curl -X POST \
     --data-binary '@nginx.ini' \
     -H "Content-Type: text/plain" \
     https://diecutter/nginx/sites-available/front \
     > /etc/nginx/sites-available/front
  • Easy to automate
  • POST context data in file
  • Pick a single file in directory
  • Need to see the diff? Use mercurial or git!
Generate code

Run:

curl -X POST  \
     -d django_project=demo  \
     -H accept:application/zip \
     http://diecutter.alwaysdata.net/api/+django_project+/

You get:

.
├── demo/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

Run:

firefox http://diecutter.alwaysdata.net/sphinx-docs.html

You get:

docs/
├── conf.py
├── demo.txt
├── index.txt
└── Makefile
  • All you need is an HTTP client
  • Use community templates or your own
  • Dynamic filenames
  • Dynamic trees
  • Online HTML client => user-friendly form
Adapt
Multiple template engines
  • Jinja
  • Cheetah
  • Mako
  • XML+XSLT
  • ERB...
Templates anywhere
  • Local files
  • Online code repositories
  • URL
KISS

Authentication, data validation, client... can be setup by combination with other tools (frontends, wsgi...)

Easy to deploy
  • Ready to use SAAS platform
  • Simple setup of private or local servers
  • Open source
REST API
  • GET, PUT, DELETE templates
  • POST data
  • Display OPTIONS, PATCH defaults
Built as a framework
  • Consistent defaults
  • Fully configurable
  • Extensible
Credits, license

https://diecutter.readthedocs.org

diecutter is licensed under BSD

  1. 2012-2013, Benoît Bryon & Rémy Hubscher

FAQ

Here are some frequently asked questions...

Why “diecutter” name?

A “die cutter” is a machine that cuts materials to produce shapes, from molds.

Here, the diecutter web service produces files from data and templates.

Does it replaces my web framework (Django) templates?

diecutter is not meant to replace web framework’s template engines! Diecutter is meant to replace file generators for configuration or scaffolding use cases.

Even if processes are really close, environment and needs aren’t. As an example, diecutter service won’t be as efficient as web frameworks internal templating systems.

So, even if, in theory, you could use diecutter as a template engine to render pages of a website, it is discouraged.

Indices and tables