{ "info": { "author": "Martijn Faassen, Startifact", "author_email": "faassen@startifact.com", "bugtrack_url": null, "classifiers": [ "Programming Language :: Python", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "hurry.custom\n============\n\nIntroduction\n------------\n\nThis package contains an infrastructure and API for the customization\nof templates. The only template languages supported by this system are\n\"pure-push\" languages which do not call into arbitrary Python code\nwhile executing. Examples of such languages are json-template\n(supported out of the box) and XSLT. The advantage of such languages\nis that they are reasonably secure to expose through-the-web\ncustomization without an elaborate security infrastructure.\n\nLet's go through the use cases that this system must support:\n\n* templates exist on the filesystem, and those are used by default.\n\n* templates can be customized. \n\n* this customization can be stored in another database (ZODB,\n filesystem, a relational database, etc); this is up to the person\n integrating ``hurry.custom``.\n\n* update template automatically if it is changed in the database.\n\n* it is possible to retrieve the template source (for display in a UI\n or for later use within for instance a web-browser for client-side\n rendering).\n\n* support server-side rendering of templates (producing HTML or an\n email message or whatever). Input is particular to template language\n (but should be considered immutable).\n\n* provide (static) input samples (such as JSON or XML files) to make\n it easier to edit and test templates. These input samples can be\n added both to the filesystem as well as to the database.\n\n* round-trip support. The customized templates and samples can be\n retrieved from the database and exported back to the\n filesystem. This is useful when templates need to be taken back\n under version control after a period of customization by end users.\n\nThe package is agnostic about (these things are pluggable):\n\n* the database used for storing customizations of templates or their\n samples.\n\n* the particular push-only template language used.\n\nWhat this package does not do is provide a user interface. It only\nprovides the API that lets you construct such user interfaces.\n\nCreating and registering a template language\n--------------------------------------------\n\nIn order to register a new push-only template we need to provide a\nfactory that takes the template text (which could be compiled down\nfurther). Instantiating the factory should result in a callable that\ntakes the input data (in whatever format is native to the template\nlanguage). The ``ITemplate`` interface defines such an object::\n\n >>> from hurry.custom.interfaces import ITemplate, CompileError, RenderError\n\nFor the purposes of demonstrating the functionality in this package,\nwe supply a very simplistic push-only templating language, based on\ntemplate strings as provided by the Python ``string`` module::\n\n >>> import string\n >>> from zope.interface import implements\n >>> class StringTemplate(object):\n ... implements(ITemplate)\n ... def __init__(self, text):\n ... if '&' in text:\n ... raise CompileError(\"& in template!\")\n ... self.source = text\n ... self.template = string.Template(text)\n ... def __call__(self, input):\n ... try:\n ... return self.template.substitute(input)\n ... except KeyError, e:\n ... raise RenderError(unicode(e))\n\nLet's demonstrate it. To render the template, simply call it with the\ndata as an argument::\n\n >>> template = StringTemplate('Hello $thing')\n >>> template({'thing': 'world'})\n 'Hello world'\n\nNote we have put some special logic in the ``__init__`` that triggers a\n``CompileError`` error if the string ``&`` is found in the\ntemplate. This is so we can easily demonstrate templates that are\nbroken - treat a template with ``&`` as a template with a syntax\n(compilation) error. Let's try it::\n\n >>> template = StringTemplate('Hello & bye')\n Traceback (most recent call last):\n ...\n CompileError: & in template!\n\nWe have also made sure we catch a possible runtime error (a\n``KeyError`` when a key is missing in the input dictionary in this\ncase) and raise this as a ``RenderError``::\n\n >>> template = StringTemplate('Hello $thing')\n >>> template({'thang': 'world'})\n Traceback (most recent call last):\n ...\n RenderError: 'thing'\n\nThe template class defines a template language. Let's register the\ntemplate language so the system is aware of it and treats ``.st`` files\non the filesystem as a string template::\n\n >>> from hurry import custom\n >>> custom.register_language(StringTemplate, extension='.st')\n\nLoading a template from the filesystem\n--------------------------------------\n\n``hurry.custom`` assumes that any templates that can be customized\nreside on the filesystem primarily and are shipped along with an\napplication's source code. They form *collections*. A collection is\nsimply a directory (with possible sub-directories) that contains\ntemplates.\n\nLet's create a collection of templates on the filesystem::\n\n >>> import tempfile, os\n >>> templates_path = tempfile.mkdtemp(prefix='hurry.custom')\n\nWe create a single template, ``test1.st`` for now::\n\n >>> test1_path = os.path.join(templates_path, 'test1.st')\n >>> f = open(test1_path, 'w')\n >>> f.write('Hello $thing')\n >>> f.close()\n\nWe also create an extra template::\n\n >>> test2_path = os.path.join(templates_path, 'test2.st')\n >>> f = open(test2_path, 'w')\n >>> f.write(\"It's full of $thing\")\n >>> f.close()\n\nIn order for the system to work, we need to register this collection\nof templates on the filesystem. We need to supply a globally unique\ncollection id, the templates path, and (optionally) a title::\n\n >>> custom.register_collection(id='templates', path=templates_path)\n\nWe can now render the template::\n\n >>> custom.render('templates', 'test1.st', {'thing': 'world'})\n u'Hello world'\n\nWe'll try another template::\n\n >>> custom.render('templates', 'test2.st', {'thing': 'stars'})\n u\"It's full of stars\"\n\nWe can also look up the template object::\n\n >>> template = custom.lookup('templates', 'test1.st')\n\nWe got our proper template::\n\n >>> template({'thing': 'world'})\n u'Hello world'\n\nThe templat also has a ``source`` attribute::\n\n >>> template.source\n u'Hello $thing'\n\nThe source text of the template was interpreted as a UTF-8 string. The\ntemplate source should always be in unicode format (or in plain\nASCII).\n\nThe underlying template will not be reloaded unless it is changed on\nthe filesystem::\n\n >>> orig = template.template\n\nWhen we trigger a potential reload nothing happens - the template did\nnot change on the filesystem::\n\n >>> template.source\n u'Hello $thing'\n >>> template.template is orig\n True\n \nIt will however automatically reload the template when it has changed\non the filesystem. We will demonstrate that by modifying the file::\n\n >>> f = open(test1_path, 'w')\n >>> f.write('Bye $thing')\n >>> f.close()\n\nUnfortunately this won't work in the tests as the modification time of\nfiles has a second-granularity on some platforms, way too long to\ndelay the tests for. We will therefore manually update the last updated\ntime as a hack::\n\n >>> template._last_updated -= 1\n\nNow the template will have changed::\n\n >>> template.source\n u'Bye $thing'\n \n >>> template({'thing': 'world'})\n u'Bye world'\n\nCustomization database\n----------------------\n\nSo far all our work was done in the root (filesystem) database. We can\nget it now::\n\n >>> root_db = custom.root_collection('templates')\n\nBefore any customization database was registered we could also have\ngotten it using ``custom.collection``, which gets the collection in\ncontext::\n\n >>> custom.collection('templates') is root_db\n True\n \nLet's now register a customization database for our collection, in a\nparticular site. This means in such a site, the new customized\ntemplate database will be used (with a fallback on the original one if\nno customization can be found or if there is an error in the use of a\ncustomization).\n\nLet's create a site first::\n\n >>> site1 = DummySite(id=1)\n\nWe register a customization database for our collection named\n``templates``. For the purposes of testing we will use an in-memory\ndatabase::\n\n >>> mem_db = custom.InMemoryTemplateDatabase('templates', 'Templates')\n >>> from hurry.custom.interfaces import ITemplateDatabase\n >>> sm1 = site1.getSiteManager()\n >>> sm1.registerUtility(mem_db, provided=ITemplateDatabase, \n ... name='templates')\n\nWe go into this site::\n\n >>> setSite(site1)\n\nWe can now find this collection using ``custom.collection``::\n\n >>> custom.collection('templates') is mem_db\n True\n\nThe collection below it is the root collection::\n\n >>> custom.next_collection('templates', mem_db) is root_db\n True\n\nBelow this, there is no collection and we'll get a lookup error::\n\n >>> custom.next_collection('templates', root_db)\n Traceback (most recent call last):\n ...\n ComponentLookupError: No collection available for: templates\n\nWe haven't placed any customization in the customization database\nyet, so we'll see the same thing as before when we look up the\ntemplate::\n\n >>> custom.render('templates', 'test1.st', {'thing': \"universe\"})\n u'Bye universe'\n\nCustomization of a template\n---------------------------\n\nNow that we have a locally set up customization database, we can\ncustomize the ``test1.st`` template. \n\nIn this customization we change 'Bye' to 'Goodbye'::\n\n >>> source = root_db.get_source('test1.st')\n >>> source = source.replace('Bye', 'Goodbye')\n\nWe now need to update the database so that it has this customized\nversion of the template. We do this by calling the ``update`` method\non the database with the template id and the new source.\n\nThis update operation is not supported on the default filesystem\ndatabase::\n\n >>> root_db.update('test1.st', source)\n Traceback (most recent call last):\n ...\n NotSupported: Cannot update templates in FilesystemTemplateDatabase.\n\nIt is supported on the site-local in-memory database we've just\ninstalled though::\n\n >>> mem_db.update('test1.st', source)\n\nAll you need to do to hook in your own database is to implement the\n``ITemplateDatabase`` interface and register it (either globally or\nlocally in a site).\n\nLet's see whether we get the customized template now::\n\n >>> custom.render('templates', 'test1.st', {'thing': 'planet'})\n u'Goodbye planet'\n\nBroken custom template\n----------------------\n\nIf a custom template cannot be compiled, the system falls back on the\nfilesystem template instead. We construct a broken custom template by\nadding ``&`` to it::\n\n >>> original_source = root_db.get_source('test2.st')\n >>> source = original_source.replace('full of', 'filled with &')\n >>> mem_db.update('test2.st', source)\n\nWe try to render this template, but instead we'll see the original\ntemplate::\n\n >>> custom.render('templates', 'test2.st', {'thing': 'planets'})\n u\"It's full of planets\"\n\nIt could also be the case that the custom template can be compiled but\ninstead cannot be rendered. Let's construct one that expects ``thang``\ninstead of ``thing``::\n\n >>> source = original_source.replace('$thing', '$thang')\n >>> mem_db.update('test2.st', source)\n\nWhen rendering the system will notice the RenderError and fall back on\nthe original uncustomized template for rendering::\n\n >>> custom.render('templates', 'test2.st', {'thing': 'planets'})\n u\"It's full of planets\"\n\nChecking which template languages are recognized\n------------------------------------------------\n\nWe can check which template languages are recognized::\n\n >>> languages = custom.recognized_languages()\n >>> sorted(languages)\n [(u'.st', )]\n\nWhen we register another language::\n\n >>> class StringTemplate2(StringTemplate):\n ... pass\n >>> custom.register_language(StringTemplate2, extension='.st2')\n\nIt will show up too::\n\n >>> languages = custom.recognized_languages()\n >>> sorted(languages)\n [(u'.st', ), (u'.st2', )]\n\nRetrieving which templates can be customized\n--------------------------------------------\n\nFor the filesystem-level templates it is possible to get a data\nstructure that indicates which templates can be customized. This is\nuseful when constructing a UI. This data structure is designed to be\neasily useful as JSON so that a client-side UI can be constructed.\n\nLet's retrieve the customization database for our collection::\n\n >>> l = custom.structure('templates')\n >>> from pprint import pprint\n >>> pprint(l)\n [{'extension': '.st',\n 'name': 'test1',\n 'path': 'test1.st',\n 'template': 'test1.st'},\n {'extension': '.st',\n 'name': 'test2',\n 'path': 'test2.st',\n 'template': 'test2.st'}]\n\nSamples\n-------\n\nIn a customization user interface it is useful to be able to test the\ntemplate. Sometimes this can be done with live data coming from the\nsoftware, but in other cases it is more convenient to try it on some\nrepresentative sample data. This sample data needs to be in the format\nas expected as the argument when calling the template.\n\nJust like a template language is stored as plain text on the\nfilesystem, sample data can also be stored as plain text on the file\nsystem. The format of this plain text is its data language. Examples\nof data languages are JSON and XML.\n\nFor the purposes of demonstration, we'll define a simle data language\nthat can turn into a dictionary a data file with key value pairs like\nthis::\n\n >>> data = \"\"\"\\\n ... a: b\n ... c: d\n ... e: f\n ... \"\"\"\n\nNow we define a function that can parse this data into a dictionary::\n\n >>> def parse_dict_data(data):\n ... result = {}\n ... for line in data.splitlines():\n ... key, value = line.split(':')\n ... key = key.strip()\n ... value = value.strip()\n ... result[key] = value\n ... return result\n >>> d = parse_dict_data(data)\n >>> sorted(d.items())\n [('a', 'b'), ('c', 'd'), ('e', 'f')]\n\nThe idea is that we can ask a particular template for those sample inputs\nthat are available for it. Let's for instance check for sample inputs \navailable for ``test1.st``::\n\n >>> root_db.get_samples('test1.st')\n {}\n\nThere's nothing yet.\n\nIn order to get samples to work, we first need to register the data\nlanguage::\n\n >>> custom.register_data_language(parse_dict_data, '.d')\n\nFiles with the extension ``.d`` can now be recognized as containing\nsample data.\n\nWe still need to tell the system that StringTemplate templates in\nparticular can be expected to find sample data with this extension. In\norder to express this, we need to register the StringTemplate language\nagain with an extra argument that indicates this (``sample_extension``)::\n\n >>> custom.register_language(StringTemplate,\n ... extension='.st', sample_extension='.d')\n\nNow we can actually look for samples. Of course there still aren't\nany as we haven't created any ``.d`` files yet::\n\n >>> root_db.get_samples('test1.st')\n {}\n\nWe need a pattern to associate a sample data file with a template\nfile. The convention used is that a sample data file is in the same\ndirectory as the template file, and starts with the name of the\ntemplate followed by a dash (``-``). Following the dash should be the\nname of the sample itself. Finally, the extension should be the sample\nextension. Here we create a sample file for the ``test1.st``\ntemplate::\n\n >>> test1_path = os.path.join(templates_path, 'test1-sample1.d')\n >>> f = open(test1_path, 'w')\n >>> f.write('thing: galaxy')\n >>> f.close()\n\nNow when we ask for the samples available for our ``test1`` template,\nwe should see ``sample1``::\n\n >>> r = root_db.get_samples('test1.st')\n >>> r\n {'sample1': {'thing': 'galaxy'}}\n\nBy definition, we can use the sample data for a template and pass it\nto the template itself::\n\n >>> template = custom.lookup('templates', 'test1.st')\n >>> template(r['sample1'])\n u'Goodbye galaxy'\n\nTesting a template\n------------------\n\nIn a user interface it can be useful to be able to test whether the\ntemplate compiles and renders. ``hurry.custom`` therefore implements a\n``check`` function that does so. This function raises an error\n(``CompileError`` or ``RenderError``), and passes silently if there is no\nproblem.\n\nLet's first try it with a broken template::\n\n >>> custom.check('templates', 'test1.st', 'foo & bar')\n Traceback (most recent call last):\n ...\n CompileError: & in template!\n\nWe'll now try it with a template that does compile but doesn't work\nwith ``sample1``, as no ``something`` is supplied::\n\n >>> custom.check('templates', 'test1.st', 'hello $something')\n Traceback (most recent call last):\n ...\n RenderError: 'something'\n\nError handling\n--------------\n\nLet's try to render a template in a collection that doesn't exist. We\nget a message that the template database could not be found::\n\n >>> custom.render('nonexistent', 'dummy.st', {})\n Traceback (most recent call last):\n ...\n ComponentLookupError: (, 'nonexistent')\n\nLet's render a non-existent template in an existing database. We get\nthe lookup error of the deepest database, which is assumed to be the\nfilesystem::\n\n >>> custom.render('templates', 'nonexisting.st', {})\n Traceback (most recent call last):\n ...\n IOError: [Errno 2] No such file or directory: '.../nonexisting.st'\n\nLet's render a template with an unrecognized extension::\n\n >>> custom.render('templates', 'dummy.unrecognized', {})\n Traceback (most recent call last):\n ...\n ComponentLookupError: (, '.unrecognized')\n\nThe template language ``.unrecognized`` could not be found. Let's make the\nfile exist; we should get the same result::\n\n >>> unrecognized = os.path.join(templates_path, 'dummy.unrecognized')\n >>> f = open(unrecognized, 'w')\n >>> f.write('Some weird template language')\n >>> f.close()\n\nNow let's look at it again::\n\n >>> template = custom.render('templates', 'dummy.unrecognized', {})\n Traceback (most recent call last):\n ...\n ComponentLookupError: (, '.unrecognized')\n\nIf we try to look up a template in the root collection with a\nCompileError in it, we'll get a CompileError::\n\n >>> compile_error = os.path.join(templates_path, 'compileerror.st')\n >>> f = open(compile_error, 'w')\n >>> f.write('A & compile error')\n >>> f.close()\n >>> compile_error_template = custom.lookup('templates', 'compileerror.st')\n Traceback (most recent call last):\n ...\n CompileError: & in template!\n\nThe same applies to trying to render it::\n\n >>> custom.render('templates', 'compileerror.st', {})\n Traceback (most recent call last):\n ...\n CompileError: & in template!\n\nIf we try to render a template in the root collection we get a RenderError::\n \n >>> render_error = os.path.join(templates_path, 'rendererror.st')\n >>> f = open(render_error, 'w')\n >>> f.write('A $thang')\n >>> f.close()\n >>> custom.render('templates', 'rendererror.st', {'thing': 'thing'})\n Traceback (most recent call last):\n ...\n RenderError: u'thang'\n\n\nWe'll get a ComponentLookupError if we look for a collection with an\nunknown id::\n\n >>> custom.collection('unknown_id')\n Traceback (most recent call last):\n ...\n ComponentLookupError: (, 'unknown_id')\n\nWe also can't look for a next collection if the id we specify is\nunknown::\n\n >>> custom.next_collection('unknown_id', mem_db)\n Traceback (most recent call last):\n ...\n ComponentLookupError: No more utilities for , 'unknown_id' have been found.\n\nSimilarly we can't get a root collection if the id is unknown::\n\n >>> custom.root_collection('unknown_id')\n Traceback (most recent call last):\n ...\n ComponentLookupError: (, 'unknown_id')\n\n\nChanges\n-------\n\n0.6.2 (2009-06-15)\n~~~~~~~~~~~~~~~~~~\n\n* Both ``RenderError`` and ``CompileError`` subclass from a common \n ``Error`` base class.\n\n0.6.1 (2009-06-15)\n~~~~~~~~~~~~~~~~~~\n\n* ``structure`` functionality now skips directories and files start that\n with a period.\n\n* If data language is not known, don't return any samples.\n\n0.6 (2009-06-10)\n~~~~~~~~~~~~~~~~\n\n* Introduce the notion of ``CompileError`` and ``RenderError``. A\n ``CompileError`` should be raised by a template if the template\n cannot be parsed or compiled. A ``RenderError`` should be raised if\n there is any run-time error during template rendering.\n\n* Introduce ``render`` in the API and de-emphasize the use of ``lookup``.\n Normally templates are rendered by calling ``render``.\n\n* When a template is looked up and there is a ``CompileError`` during\n its creation, fall back on original template.\n\n* When a template is rendered using the top-level ``render`` function\n and there is a ``RenderError`` during the rendering process, fall\n back on the original template.\n\n* Remove ``original_source`` and ``samples`` methods from\n ``IManagedTemplate`` interface. These are better handled by directly\n using the ``ITemplateDatabase`` API.\n\n* Some fixes in the interfaces, bringing them more inline with the code.\n\n* Expose ``collection``, ``next_collection`` and ``root_collection``\n functions.\n\n0.5 (2009-05-22)\n~~~~~~~~~~~~~~~~\n\n* Initial public release.\n\nDownload\n========", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "UNKNOWN", "keywords": "template custom templating customization", "license": "ZPL 2.1", "maintainer": null, "maintainer_email": null, "name": "hurry.custom", "package_url": "https://pypi.org/project/hurry.custom/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/hurry.custom/", "project_urls": { "Download": "UNKNOWN", "Homepage": "UNKNOWN" }, "release_url": "https://pypi.org/project/hurry.custom/0.6.2/", "requires_dist": null, "requires_python": null, "summary": "A framework for allowing customizing templates", "version": "0.6.2" }, "last_serial": 793112, "releases": { "0.5": [ { "comment_text": "", "digests": { "md5": "b31d9830ba4e66f63a10421842535d72", "sha256": "84418b6cd33ca239adf003d099c04f5b5e297c9af87cbf392fa7d350b4e2731d" }, "downloads": -1, "filename": "hurry.custom-0.5.tar.gz", "has_sig": false, "md5_digest": "b31d9830ba4e66f63a10421842535d72", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 16555, "upload_time": "2009-05-22T21:41:47", "url": "https://files.pythonhosted.org/packages/6a/70/dba6860b8698bcb43f056870f1a42723559356e42098a89295c36b60a9a4/hurry.custom-0.5.tar.gz" } ], "0.6": [ { "comment_text": "", "digests": { "md5": "e03735e731dcbe0ec94c1283a55e49d5", "sha256": "a5680509b46ecca2ff92db3eb7da18ffe314673ae1ee7fc2eb1195054158fcfb" }, "downloads": -1, "filename": "hurry.custom-0.6.tar.gz", "has_sig": false, "md5_digest": "e03735e731dcbe0ec94c1283a55e49d5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26063, "upload_time": "2009-06-10T15:43:04", "url": "https://files.pythonhosted.org/packages/8b/f0/deb04bf54792ed505d99ab866447b335d29f526c8edc8f00f470c82b4d9c/hurry.custom-0.6.tar.gz" } ], "0.6.1": [ { "comment_text": "", "digests": { "md5": "4cadbaa190a289137be7636f366bbe26", "sha256": "9aa32e617311d9f3aff813a05403a6191e7b01bacd6dca34572d671bccb76f51" }, "downloads": -1, "filename": "hurry.custom-0.6.1.tar.gz", "has_sig": false, "md5_digest": "4cadbaa190a289137be7636f366bbe26", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26180, "upload_time": "2009-06-15T15:26:09", "url": "https://files.pythonhosted.org/packages/78/9f/08a245b7b2fe27b5667fc8f27d679c6e7b3dc65e30bfddcc91389cda9c36/hurry.custom-0.6.1.tar.gz" } ], "0.6.2": [ { "comment_text": "", "digests": { "md5": "625b638fa2f6b6dc3b04819885c7fa3e", "sha256": "f87e1bba62b8da2b55e0887cd8465e144e88d1aa3bac02f1b4619185a9b32977" }, "downloads": -1, "filename": "hurry.custom-0.6.2.tar.gz", "has_sig": false, "md5_digest": "625b638fa2f6b6dc3b04819885c7fa3e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26271, "upload_time": "2009-06-15T15:49:20", "url": "https://files.pythonhosted.org/packages/df/7c/cbfac198086bebc8fddb7463bd4719b0699104598ec275072352af37daef/hurry.custom-0.6.2.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "625b638fa2f6b6dc3b04819885c7fa3e", "sha256": "f87e1bba62b8da2b55e0887cd8465e144e88d1aa3bac02f1b4619185a9b32977" }, "downloads": -1, "filename": "hurry.custom-0.6.2.tar.gz", "has_sig": false, "md5_digest": "625b638fa2f6b6dc3b04819885c7fa3e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 26271, "upload_time": "2009-06-15T15:49:20", "url": "https://files.pythonhosted.org/packages/df/7c/cbfac198086bebc8fddb7463bd4719b0699104598ec275072352af37daef/hurry.custom-0.6.2.tar.gz" } ] }