#!/usr/bin/python
import getopt, sys
usage="""
This script will populate a Cheetah template ((http://www.cheetahtemplate.org/)
with some input data (XML, CSV or JSON format).
By default, the output is directed to stdout.
USAGE:
template [ -o outputFile ] [options] <template_file> <data_file>
template [-h/--help]
ARGUMENTS:
<template_file> : Filename for the template file. Can be "stdin"
<data_file> : Filename for input data. Can be "stdin"
OPTIONS:
-o <output file> Direct output in a file instead of stdout.
-c 'commentChar' Change the character used to begin comments in the template.
-d 'directiveChar' Change the character used for directives in the template.
-t XML|CSV Input type (or guessed with file extension)
"""
def dieWith(msg) :
sys.stderr.write(msg + '\n')
sys.exit(-1)
# Enum of data type
TYPE_NONE = 0
TYPE_XML = 2
TYPE_CSV = 3
# Parse options
try:
opts, args = getopt.getopt(sys.argv[1:], "hc:d:t:o:", ["help"])
except getopt.GetoptError :
print usage
# Init arguments / options
compilerSettings = {}
inputType = TYPE_NONE
outFilename = None
# Switch on options
for opt, arg in opts:
# Help
if opt in ("-h", "--help") :
print usage
sys.exit(0)
# Comment char
elif opt == "-c" :
compilerSettings['commentStartToken'] = arg
# Directive char
elif opt == "-d" :
compilerSettings['directiveStartToken'] = arg
# Output file
elif opt == "-o" :
outFilename = arg
# Input type
elif opt == "-t" :
arg= arg.lower()
if arg == "csv" :
inputType = TYPE_CSV
elif arg == "xml" :
inputType = TYPE_XML
else :
dieWith("Invalid input type. Valid options are : CSV, XML")
# 2 mandatory arguments
if len(args) != 2:
print usage
sys.exit(2);
(templateFile, dataFile) = args
# --------------------------------------------------------------------------
# XML to Python Object parser
# --------------------------------------------------------------------------
## {{{ http://code.activestate.com/recipes/534109/ (r8)
## Created by Wai Yip Tung on Sat, 13 Oct 2007
import re
import xml.sax.handler
def xml2obj(src):
"""
A simple function to converts XML data into native Python object.
"""
non_id_char = re.compile('[^_0-9a-zA-Z]')
def _name_mangle(name):
return non_id_char.sub('_', name)
class DataNode(object):
def __init__(self):
self._attrs = {} # XML attributes and child elements
self.data = None # child text data
def __len__(self):
# treat single element as a list of 1
return 1
def __getitem__(self, key):
if isinstance(key, basestring):
return self._attrs.get(key,None)
else:
return [self][key]
def __contains__(self, name):
return self._attrs.has_key(name)
def __nonzero__(self):
return bool(self._attrs or self.data)
def __getattr__(self, name):
if name.startswith('__'):
# need to do this for Python special methods???
raise AttributeError(name)
return self._attrs.get(name,None)
def _add_xml_attr(self, name, value):
if name in self._attrs:
# multiple attribute of the same name are represented by a list
children = self._attrs[name]
if not isinstance(children, list):
children = [children]
self._attrs[name] = children
children.append(value)
else:
self._attrs[name] = value
def __str__(self):
return self.data or ''
def __repr__(self):
items = sorted(self._attrs.items())
if self.data:
items.append(('data', self.data))
return u'{%s}' % ', '.join([u'%s:%s' % (k,repr(v)) for k,v in items])
class TreeBuilder(xml.sax.handler.ContentHandler):
def __init__(self):
self.stack = []
self.root = DataNode()
self.current = self.root
self.text_parts = []
def startElement(self, name, attrs):
self.stack.append((self.current, self.text_parts))
self.current = DataNode()
self.text_parts = []
# xml attributes --> python attributes
for k, v in attrs.items():
self.current._add_xml_attr(_name_mangle(k), v)
def endElement(self, name):
text = ''.join(self.text_parts).strip()
if text:
self.current.data = text
if self.current._attrs:
obj = self.current
else:
# a text only node is simply represented by the string
obj = text or ''
self.current, self.text_parts = self.stack.pop()
self.current._add_xml_attr(_name_mangle(name), obj)
def characters(self, content):
self.text_parts.append(content)
builder = TreeBuilder()
if isinstance(src,basestring):
xml.sax.parseString(src, builder)
else:
xml.sax.parse(src, builder)
return builder.root._attrs.values()[0]
## end of http://code.activestate.com/recipes/534109/ }}}
# -------------------------------------------------
# Read input data file
# -------------------------------------------------
# Open input file
import csv
if dataFile == "stdin" :
file = sys.stdin
else:
file = open(dataFile);
# Guess input type if not set in options
if inputType == TYPE_NONE :
import os.path as path
ext = path.splitext(dataFile)[1].lower()
if ext == '.csv' :
inputType = TYPE_CSV
elif ext == '.xml' :
inputType = TYPE_XML
elif ext == '.json' :
inputType = TYPE_JSON
# Switch on input type
if inputType == TYPE_NONE :
dieWith("No input data type specified. Failed to guess it.")
# CSV
elif inputType == TYPE_CSV :
reader = csv.DictReader(file, delimiter=";")
# Almost empty
class Container :
def __init__(self) :
self.lines= []
data = Container()
# Loop on lines
for line in reader:
data.lines.append(line)
# Loop on values of the line
for key, value in line.items() :
# Does it exists yet in "data"
if data.__dict__.has_key(key) :
# Then happend it
data.__dict__[key].append(value)
else :
# Create a list
data.__dict__[key] = [value]
# Make 'columns' accessible as a global name in the template
data.columns = reader.fieldnames
# XML
elif inputType == TYPE_XML :
# Transform XML into Python object
data = xml2obj(file)
else :
dieWith('Input data type not supported')
# --------------------------------------------
# Read template
# --------------------------------------------
from Cheetah.Template import Template
if templateFile == 'stdin' :
file = sys.stdin
else:
file = open(templateFile)
template = Template(
file=file,
searchList=[data], # Attach data
compilerSettings = compilerSettings)
# -------------------------------------------
# Output result
# -------------------------------------------
if outFilename == None :
out = sys.stdout
else :
out = open(outFilename, 'w')
out.write(str(template))