diff --git a/tablib/formats/_ods.py b/tablib/formats/_ods.py index c7af891d..a4bdf816 100644 --- a/tablib/formats/_ods.py +++ b/tablib/formats/_ods.py @@ -3,91 +3,59 @@ """ Tablib - ODF Support. """ -from odf import opendocument, style, table, text +from tablib.packages import ods from tablib.compat import BytesIO, unicode title = 'ods' extensions = ('ods',) -bold = style.Style(name="bold", family="paragraph") -bold.addElement(style.TextProperties(fontweight="bold", fontweightasian="bold", fontweightcomplex="bold")) def export_set(dataset): """Returns ODF representation of Dataset.""" - wb = opendocument.OpenDocumentSpreadsheet() - wb.automaticstyles.addElement(bold) - - ws = table.Table(name=dataset.title if dataset.title else 'Tablib Dataset') - wb.spreadsheet.addElement(ws) - dset_sheet(dataset, ws) - stream = BytesIO() - wb.save(stream) + wb = ods.ODSWorkbook(stream) + dset_sheet(dataset, wb, title='Sheet') + wb.close() return stream.getvalue() def export_book(databook): """Returns ODF representation of DataBook.""" - wb = opendocument.OpenDocumentSpreadsheet() - wb.automaticstyles.addElement(bold) - - for i, dset in enumerate(databook._datasets): - ws = table.Table(name=dset.title if dset.title else 'Sheet%s' % (i)) - wb.spreadsheet.addElement(ws) - dset_sheet(dset, ws) - - stream = BytesIO() - wb.save(stream) + wb = ods.ODSWorkbook(stream) + + for i, dataset in enumerate(databook._datasets): + dset_sheet(dataset, wb, title='Sheet %d' % i) + wb.close() return stream.getvalue() -def dset_sheet(dataset, ws): +def dset_sheet(dataset, wb, title=None): """Completes given worksheet from given Dataset.""" _package = dataset._package(dicts=False) + if not _package: + return + + wb.start_sheet(len(_package[0]), + dataset.title if dataset.title else title) for i, sep in enumerate(dataset._separators): _offset = i _package.insert((sep[0] + _offset), (sep[1],)) for i, row in enumerate(_package): - row_number = i + 1 - odf_row = table.TableRow(stylename=bold, defaultcellstylename='bold') - for j, col in enumerate(row): + cells = [] + for cell in row: try: - col = unicode(col, errors='ignore') + cell = unicode(cell, errors='ignore') except TypeError: - ## col is already unicode + # col is already unicode pass - ws.addElement(table.TableColumn()) - - # bold headers - if (row_number == 1) and dataset.headers: - odf_row.setAttribute('stylename', bold) - ws.addElement(odf_row) - cell = table.TableCell() - p = text.P() - p.addElement(text.Span(text=col, stylename=bold)) - cell.addElement(p) - odf_row.addElement(cell) - - # wrap the rest - else: - try: - if '\n' in col: - ws.addElement(odf_row) - cell = table.TableCell() - cell.addElement(text.P(text=col)) - odf_row.addElement(cell) - else: - ws.addElement(odf_row) - cell = table.TableCell() - cell.addElement(text.P(text=col)) - odf_row.addElement(cell) - except TypeError: - ws.addElement(odf_row) - cell = table.TableCell() - cell.addElement(text.P(text=col)) - odf_row.addElement(cell) + cells.append(cell) + if i == 0: + wb.add_headers(cells) + else: + wb.add_row(cells) + wb.end_sheet() \ No newline at end of file diff --git a/tablib/packages/loxun.py b/tablib/packages/loxun.py new file mode 100644 index 00000000..9b6726c2 --- /dev/null +++ b/tablib/packages/loxun.py @@ -0,0 +1,1487 @@ +""" +loxun is a Python module to write large output in XML using Unicode and +namespaces. Of course you can also use it for small XML output with plain 8 +bit strings and no namespaces. + +loxun's features are: + +* **small memory foot print**: the document is created on the fly by writing to + an output stream, no need to keep all of it in memory. + +* **easy to use namespaces**: simply add a namespace and refer to it using the + standard ``namespace:tag`` syntax. + +* **mix unicode and io.BytesIO**: pass both unicode or plain 8 bit strings to any + of the methods. Internally loxun converts them to unicode, so once a + parameter got accepted by the API you can rely on it not causing any + messy ``UnicodeError`` trouble. + +* **automatic escaping**: no need to manually handle special characters such + as ``<`` or ``&`` when writing text and attribute values. + +* **robustness**: while you write the document, sanity checks are performed on + everything you do. Many silly mistakes immediately result in an + ``XmlError``, for example missing end elements or references to undeclared + namespaces. + +* **open source**: distributed under the GNU Lesser General Public License 3 + or later. + +Here is a very basic example. First you have to create an output stream. In +many cases this would be a file, but for the sake of simplicity we use a +``io.BytesIO`` here: + + >>> from __future__ import unicode_literals + >>> import io + >>> out = io.BytesIO() + +Then you can create an `XmlWriter` to write to this output: + + >>> xml = XmlWriter(out) + +Now write the content: + + >>> xml.addNamespace("xhtml", "http://www.w3.org/1999/xhtml") + >>> xml.startTag("xhtml:html") + >>> xml.startTag("xhtml:body") + >>> xml.text("Hello world!") + >>> xml.tag("xhtml:img", {"src": "smile.png", "alt": ":-)"}) + >>> xml.endTag() + >>> xml.endTag() + >>> xml.close() + +And the result is: + + >>> print out.getvalue().rstrip("\\r\\n") + + + + Hello world! + + + + +Writing a simple document +========================= + +The following example creates a very simple XHTML document. + +To make it simple, the output goes to a ``BytesIO``, but you could also use +a binary file that has been created using ``io.open(filename, "wb")``. + + >>> from __future__ import unicode_literals + >>> import io + >>> out = io.BytesIO() + +First create an `XmlWriter` to write the XML code to the specified output: + + >>> xml = XmlWriter(out) + +This automatically adds the XML prolog: + + >>> print out.getvalue().rstrip("\\r\\n") + + +Next add the ```` start tag: + + >>> xml.startTag("html") + +Now comes the . To pass attributes, specify them in a dictionary. +So in order to add:: + + + +use: + + >>> xml.startTag("body", {"id": "top"}) + +Let' add a little text so there is something to look at: + + >>> xml.text("Hello world!") + +Wrap it up: close all elements and the document. + + >>> xml.endTag() + >>> xml.endTag() + >>> xml.close() + +And this is what we get: + + >>> print out.getvalue().rstrip("\\r\\n") + + + + Hello world! + + + +Specifying attributes + +First create a writer: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out) + +Now write the content: + + >>> xml.tag("img", {"src": "smile.png", "alt": ":-)"}) + +Attribute values do not have to be strings, other types will be converted to +Unicode using Python's ``unicode()`` function: + + >>> xml.tag("img", {"src": "wink.png", "alt": ";-)", "width": 32, "height": 24}) + +And the result is: + + >>> print out.getvalue().rstrip("\\r\\n") + + :-) + ;-) + +Using namespaces +================ + +Now the same thing but with a namespace. First create the prolog +and header like above: + + >>> out = io.BytesIO() + >>> xml = XmlWriter(out) + +Next add the namespace: + + >>> xml.addNamespace("xhtml", "http://www.w3.org/1999/xhtml") + +Now elements can use qualified tag names using a colon (:) to separate +namespace and tag name: + + >>> xml.startTag("xhtml:html") + >>> xml.startTag("xhtml:body") + >>> xml.text("Hello world!") + >>> xml.endTag() + >>> xml.endTag() + >>> xml.close() + +As a result, tag names are now prefixed with "xhtml:": + + >>> print out.getvalue().rstrip("\\r\\n") + + + + Hello world! + + + +Working with non ASCII characters +================================= + +Sometimes you want to use characters outside the ASCII range, for example +German Umlauts, the Euro symbol or Japanese Kanji. The easiest and performance +wise best way is to use Unicode strings. For example: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + >>> xml.text(u"The price is \\u20ac 100") # Unicode of Euro symbol + >>> out.getvalue().rstrip("\\r\\n") + 'The price is \\xe2\\x82\\xac 100' + +Notice the "u" before the string passed to `XmlWriter.text()`, it declares the +string to be a unicode string that can hold any character, even those that are +beyond the 8 bit range. + +Also notice that in the output the Euro symbol looks very different from the +input. This is because the output encoding is UTF-8 (the default), which +has the advantage of keeping all ASCII characters the same and turning any +characters with a code of 128 or more into a sequence of 8 bit bytes that +can easily fit into an output stream to a binary file or ``io.BytesIO``. + +If you have to stick to classic 8 bit string parameters, loxun attempts to +convert them to unicode. By default it assumes ASCII encoding, which does +not work out as soon as you use a character outside the ASCII range: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + >>> xml.text("The price is \\xa4 100") # ISO-8859-15 code of Euro symbol + Traceback (most recent call last): + ... + UnicodeDecodeError: 'ascii' codec can't decode byte 0xa4 in position 13: ordinal not in range(128) + +In this case you have to tell the writer the encoding you use by specifying +the the ``sourceEncoding``: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False, sourceEncoding="iso-8859-15") + +Now everything works out again: + + >>> xml.text("The price is \\xa4 100") # ISO-8859-15 code of Euro symbol + >>> out.getvalue().rstrip("\\r\\n") + 'The price is \\xe2\\x82\\xac 100' + +Of course in practice you will not mess around with hex codes to pass your +texts. Instead you just specify the source encoding using the mechanisms +described in PEP 263, +`Defining Python Source Code Encodings `_. + +Pretty printing and indentation +=============================== + +By default, loxun starts a new line for each ``startTag`` and indents the +content with two spaces. You can change the spaces to any number of spaces and +tabs you like: + + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, indent=" ") # <-- Indent with 4 spaces. + >>> xml.startTag("html") + >>> xml.startTag("body") + >>> xml.text("Hello world!") + >>> xml.endTag() + >>> xml.endTag() + >>> xml.close() + >>> print out.getvalue().rstrip("\\r\\n") + + + + Hello world! + + + +You can disable pretty printing all together using ``pretty=False``, resulting +in an output of a single large line: + + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, pretty=False) # <-- Disable pretty printing. + >>> xml.startTag("html") + >>> xml.startTag("body") + >>> xml.text("Hello world!") + >>> xml.endTag() + >>> xml.endTag() + >>> xml.close() + >>> print out.getvalue().rstrip("\\r\\n") + Hello world! + +Changing the XML prolog +======================= + +When you create a writer, it automatically write an XML prolog +processing instruction to the output. This is what the default prolog +looks like: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out) + >>> print out.getvalue().rstrip("\\r\\n") + + +You can change the version or encoding: + + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, encoding=u"ascii", version=u"1.1") + >>> print out.getvalue().rstrip("\\r\\n") + + +To completely omit the prolog, set the parameter ``prolog=False``: + + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + >>> out.getvalue() + '' + +Adding other content +==================== + +Apart from text and tags, XML provides a few more things you can add to +documents. Here's an example that shows how to do it with loxun. + +First, create a writer: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out) + +Let's add a document type definition: + + >>> xml.raw("") + >>> xml.newline() + +Notice that loxun uses the generic `XmlWriter.raw()` for that, which allows to +add any content without validation or escaping. You can do all sorts of nasty +things with ``raw()`` that will result in invalid XML, but this is one of its +reasonable uses. + +Next, let's add a comment: + + >>> xml.comment("Show case some rarely used XML constructs") + +Here is a processing instruction: + + >>> xml.processingInstruction("xml-stylesheet", "href=\\"default.css\\" type=\\"text/css\\"") + +And finally a CDATA section: + + >>> xml.cdata(">> this will not be parsed <<") + +And the result is: + + >>> print out.getvalue().rstrip("\\r\\n") + + + + + > this will not be parsed <<]]> + + +Optimization +============ + +Loxun automatically optimized pairs of empty start/end tags. For example: + + >>> out = io.BytesIO() + >>> xml = XmlWriter(out) + >>> xml.startTag("customers") + >>> xml.startTag("person", {"id": "12345", "name": "Doe, John"}) + >>> xml.endTag("person") # without optimization, this would add . + >>> xml.endTag() + >>> xml.close() + >>> print out.getvalue().rstrip("\\r\\n") + + + + + +Despite the explicit ``startTag("person")`` and matching ``endtag()``, the +output only contains a simple ```` tag. + +Contributing +------------ + +If you want to help improve loxun, you can access the source code at +. + +Future +====== + +Currently loxun does what it was built for. + +There are is no real plans to improve it in the near future, but here is a list +of features that might be added at some point: + +* Add validation of tag and attribute names to ensure that all characters used + are allowed. For instance, currently loxun does not complain about a tag + named "a#b*c$d_". +* Raise an `XmlError` when namespaces are added with attributes instead of + `XmlWriter.addNamespace()`. +* Logging support to simplify debugging of the calling code. Probably + `XmlWriter` would get a property ``logger`` which is a standard + ``logging.Logger``. By default it could log original exceptions that + loxun turns into an `XmlError` and namespaces opened and closed. + Changing it to ``logging.DEBUG`` would log each tag and XML construct + written, including additional information about the internal tag stack. + That way you could dynamically increase or decrease logging output. +* Rethink pretty printing. Instead of a global property that can only be set + when initializing an `XmlWriter`, it could be a optional parameter for + `XmlWriter.startTag()` where it could be turned on and off as needed. And + the property could be named ``literal`` instead of ``pretty`` (with an + inverse logic). +* Add a ``DomWriter`` that creates a ``xml.dom.minidom.Document``. + +Some features other XML libraries support but I never saw any real use for: + +* Specify attribute order for tags. + +Version history +=============== + +Version 2.0, 2014-07-28 + +* Added support for Python 3.2+ while retaining the option to run with + Python 2.6+ (issue #5; thanks go to `Stefan Schwarzer`_ who offered his + guidance during a "Python 2 to 3" sprint at EuroPython 2014). +* Dropped support for Python 2.5, keep using loxun 1.3 if you are stuck + with with this version. + +.. _Stefan Schwarzer: http://www.sschwarzer.net + +Version 1.3, 2012-01-01 + +* Added ``endTags()`` to close several or all open tags (issue #3, + contributed by Anton Kolechkin). +* Added ``ChainXmlWriter`` which is similar to ``XmlWriter`` and allows to + chain methods for more concise source code (issue #3, contributed by Anton + Kolechkin). + +Version 1.2, 2011-03-12 + +* Fixed ``AttributeError`` when ``XmlWriter(..., encoding=...)`` was set. + +Version 1.1, 08-Jan-2011 + +* Fixed ``AssertionError`` when ``pretty`` was set to ``False`` + (issue #1; fixed by David Cramer). + +Version 1.0, 11-Oct-2010 + +* Added support for Python's ``with`` so you don not have to manually call + `XmlWriter.close()` anymore. +* Added Git repository at . + +Version 0.8, 11-Jul-2010 + +* Added possibility to pass attributes to `XmlWriter.startTag()` and + `XmlWriter.tag()` with values that have other types than ``str`` or + ``unicode``. When written to XML, the value is converted using Python's + built-in ``unicode()`` function. +* Added a couple of files missing from the distribution, most important the + test suite. + +Version 0.7, 03-Jul-2010 + +* Added optimization of matching start and end tag without any content in + between. For example, ``x.startTag("some"); x.endTag()`` results in + ```` instead of ````. +* Fixed handling of unknown name spaces. They now raise an `XmlError` instead + of ``ValueError``. + +Version 0.6, 03-Jun-2010 + +* Added option ``indent`` to specify the indentation text each new line starts with. +* Added option ``newline`` to specify how lines written should end. +* Fixed that `XmlWriter.tag()` did not remove namespaces declared immediately + before it. +* Cleaned up documentation. + +Version 0.5, 25-May-2010 + +* Fixed typo in namespace attribute name. +* Fixed adding of namespaces before calls to `XmlWriter.tag()` which resulted + in an `XmlError`. + +Version 0.4, 21-May-2010 + +* Added option ``sourceEncoding`` to simplify processing of classic strings. + The manual section "Working with non ASCII characters" explains how to use + it. + +Version 0.3, 17-May-2010 + +* Added scoped namespaces which are removed automatically by + `XmlWriter.endTag()`. +* Changed ``text()`` to normalize newlines and white space if pretty printing + is enabled. +* Moved writing of XML prolog to the constructor and removed + ``XmlWriter.prolog()``. To omit the prolog, specify ``prolog=False`` when + creating the `XmlWriter`. If you later want to write the prolog yourself, + use `XmlWriter.processingInstruction()`. +* Renamed ``*Element()`` to ``*Tag`` because they really only write tags, not + whole elements. + +Version 0.2, 16-May-2010 + +* Added `XmlWriter.comment()`, `XmlWriter.cdata()` and + `XmlWriter.processingInstruction()` to write these specific XML constructs. +* Added indentation and automatic newline to text if pretty printing is + enabled. +* Removed newline from prolog in case pretty printing is disabled. +* Fixed missing "?" in prolog. + +Version 0.1, 15-May-2010 + +* Initial release. +""" +# Copyright (C) 2010-2012 Thomas Aglassinger +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +# License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +from __future__ import unicode_literals + +import collections +import io +import os +import re +import sys +import xml.sax.saxutils + +__version__ = "2.0" + + +# Compatibility helpers for Python 2 and 3. +if sys.version_info[0] == 2: + bytes_type = str + unicode_type = unicode +else: + bytes_type = bytes + unicode_type = str + + +class XmlError(Exception): + """ + Error raised when XML can not be generated. + """ + pass + +def _quoted(value): + _assertIsUnicode("value", value) + return xml.sax.saxutils.quoteattr(value) + +def _validateNotEmpty(name, value): + """ + Validate that ``value`` is not empty and raise `XmlError` in case it is. + """ + assert name + if not value: + raise XmlError("%s must not be empty" % name) + +def _validateNotNone(name, value): + """ + Validate that ``value`` is not ``None`` and raise `XmlError` in case it is. + """ + assert name + if value is None: + raise XmlError("%s must not be %r" % (name, None)) + +def _validateNotNoneOrEmpty(name, value): + """ + Validate that ``value`` is not empty or ``None`` and raise `XmlError` in case it is. + """ + _validateNotNone(name, value) + _validateNotEmpty(name, value) + +def _assertIsUnicode(name, value): + assert (value is None) or isinstance(value, unicode_type), \ + "value for %r must be of type %s but is: %r" % (name, unicode_type.__name__, value) + +def _splitPossiblyQualifiedName(name, value): + """ + A pair ``(namespace, name)`` derived from ``name``. + + A fully qualified name: + + >>> _splitPossiblyQualifiedName(u"tag name", u"xhtml:img") + (u'xhtml', u'img') + + A name in the default namespace: + + >>> _splitPossiblyQualifiedName(u"tag name", u"img") + (None, u'img') + + Improper names result in an `XmlError`: + + >>> _splitPossiblyQualifiedName(u"x", u"") + Traceback (most recent call last): + ... + XmlError: x must not be empty + """ + assert name + _assertIsUnicode("name", name) + _assertIsUnicode("value", value) + + colonIndex = value.find(":") + if colonIndex == -1: + _validateNotEmpty(name, value) + result = (None, value) + else: + namespacePart = value[:colonIndex] + _validateNotEmpty("namespace part of %s", namespacePart) + namePart = value[colonIndex+1:] + _validateNotEmpty("name part of %s", namePart) + result = (namespacePart, namePart) + # TODO: validate that all parts are NCNAMEs. + return result + +def _joinPossiblyQualifiedName(namespace, name): + _assertIsUnicode("namespace", namespace) + assert name + _assertIsUnicode("name", name) + if namespace: + result = "%s:%s" % (namespace, name) + else: + result = name + return result + +class XmlWriter(object): + """ + Writer for large output in XML optionally supporting Unicode and + namespaces. + """ + # Marks to start/end CDATA. + _CDATA_START = "" + + # Marks to start/end processing instruction. + _PROCESSING_START = "" + + # Possible value for _possiblyWriteTag()'s ``close`` parameter. + _CLOSE_NONE = "none" + _CLOSE_AT_START = "start" + _CLOSE_AT_END = "end" + + # Build regular expressions to validate tag and attribute names. + _NAME_START_CHARS = "_a-zA-Z\u00c0-\u00d6\u00d8-\u00f6\00f8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c-\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd" + _NAME_CHARS = "\\-\\.0-9" + _NAME_START_CHARS + "\\u00b7\\u0300-\\u036f\\u203f-\\u2040" + _NAME_START_CHAR_PATTERN = "[" + _NAME_START_CHARS + "]" + _NAME_CHAR_PATTERN = "[" + _NAME_CHARS + "]" + _nameStartCharRegEx = re.compile(_NAME_START_CHAR_PATTERN, re.UNICODE) + _nameCharRegEx = re.compile(_NAME_START_CHAR_PATTERN, re.UNICODE) + + def __init__(self, output, pretty=True, indent=" ", newline=os.linesep, encoding="utf-8", errors="strict", prolog=True, version="1.0", sourceEncoding="ascii"): + """ + Initialize ``XmlWriter`` writing to ``output``. + + The ``output`` can be anything that has a ``write(data)`` method, + typically a filelike object. The writer accesses the ``output`` as + stream, so it does not have to support any methods for random + access like ``seek()``. + + In case you write to a file, use ``"wb"`` as ``mode`` for ``open()`` + to prevent messed up newlines. + + Set ``pretty`` to ``False`` if you do not want to the writer to pretty + print. Keep in mind though that this results in the whole output being + a single line unless you use `newline()` or write text with newline + characters in it. + + Set ``indent`` to the string that should be used for each indentation + level. + + Set ``newline`` to the string that should be used at the end of each + line. + + Set ``encoding`` to the name of the preferred output encoding. + + Set ``errors`` to one of the value possible value for + ``unicode(..., error=...)`` if you do not want the output to fail with + a `UnicodeError` in case a character cannot be encoded. + + Set ``prolog`` to ``False`` if you do not want the writer to emit an + XML prolog processing instruction (like + ````). + + Set ``version`` to the value the version attribute in the XML prolog + should have. + + Set ``sourceEncoding`` to the name of the encoding that plain 8 bit + strings passed as parameters use. + """ + assert output is not None + assert encoding + assert errors + assert sourceEncoding + _validateNotNoneOrEmpty("version", version) + self._output = output + self._pretty = pretty + self._sourceEncoding = sourceEncoding + self._encoding = self._unicodedFromString(encoding) + self._errors = self._unicodedFromString(errors) + self._namespaces = {} + self._elementStack = collections.deque() + self._namespacesToAdd = collections.deque() + self._isOpen = True + self._contentHasBeenWritten = False + self._indent = self._unicodedFromString(indent) + + # `None` or a tuple of (indent, qualifiedTagName, attributes). + # See also: `_possiblyWriteTag()`. + self._startTagToWrite = None + + indentWithoutWhiteSpace = self._indent.replace(" ", "").replace("\t", "") + assert not indentWithoutWhiteSpace, \ + "`indent` must contain only blanks or tabs but also has: %r" % indentWithoutWhiteSpace + self._newline = self._unicodedFromString(newline) + _VALID_NEWLINES = ["\r", "\n", "\r\n"] + assert self._newline in _VALID_NEWLINES, \ + "`newline` is %r but must be one of: %s" % (self._newline, _VALID_NEWLINES) + if prolog: + self.processingInstruction("xml", "version=%s encoding=%s" % ( \ + _quoted(self._unicodedFromString(version)), + _quoted(self._encoding)) + ) + + def __enter__(self): + return self + + def __exit__(self, errorType, error, traceback): + if not error: + # There's no point in calling `close()` in case of previous errors + # because it most likely will cause another error and thus discard + # the original error which holds actually useful information. + # + # Not calling `close()` will *not* introduce any resource leaks. + self.close() + + @property + def isPretty(self): + """Pretty print writes to the ``output``?""" + return self._pretty + + @property + def encoding(self): + """The encoding used when writing to the ``output``.""" + return self._encoding + + @property + def output(self): + """The stream where the output goes.""" + return self._output + + def _scope(self): + return len(self._elementStack) + + def _encoded(self, text): + assert text is not None + _assertIsUnicode("text", text) + return text.encode(self._encoding, self._errors) + + def _unicodedFromString(self, text): + """ + Same value as ``text`` but converted to unicode in case ``text`` is a + string. ``None`` remains ``None``. + """ + if text is None: + result = None + elif isinstance(text, unicode_type): + result = text + else: + result = unicode_type(text, self._sourceEncoding) + return result + + def _raiseStrOrUnicodeBroken(self, method, value, error): + """ + Raise `XmlError` pointing out the ``__str__()`` or ``__unicode__`` of + of the type of ``value`` must be implemented properly. + """ + assert method in ("str", "unicode") + assert error is not None + + someTypeName = type(value).__name__ + message = "%s.__%s()__ must return a value of type %s or %s but failed for value %r with: %s" % ( + someTypeName, method, unicode_type.__name__, bytes_type.__name__, value, error + ) + raise XmlError(message) + + def _unicoded(self, some): + """ + Same value as ``some`` but converted to unicode in case ``some`` is + not already a unicode string. ``None`` remains ``None``. + + Examples: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out) + >>> xml._unicoded(u"abc") + u'abc' + >>> xml._unicoded("abc") + u'abc' + >>> xml._unicoded(123) + u'123' + + >>> import decimal + >>> xml._unicoded(decimal.Decimal("123.45")) + u'123.45' + + In order for this to work, the type of ``some`` must have a proper + implementation of ``__unicode()__`` or ``__str__``. + + Here is an example for a class with a broken ``__unicode__``, which + already fails if ``unicode()`` is called without loxun: + + >>> class Broken(object): + ... def __unicode__(self): + ... return 123 # BROKEN: Return type must be str or unicode + >>> unicode(Broken()) + Traceback (most recent call last): + ... + TypeError: coercing to Unicode: need string or buffer, int found + + Consequently, using a value of ``Broken`` as attribute value will fail too: + + >>> xml.tag("someTag", {"someAttribute": Broken()}) #doctest: +ELLIPSIS + Traceback (most recent call last): + ... + XmlError: Broken.__unicode()__ must return a value of type str or unicode but failed for value ... with: coercing to Unicode: need string or buffer, int found + """ + if some is None: + result = None + elif isinstance(some, unicode_type): + result = some + else: + if isinstance(some, bytes_type): + result = some + else: + try: + result = unicode_type(some) + except Exception as error: + self._raiseStrOrUnicodeBroken("unicode", some, error) + # Ensure that the caller implemented __str__ / __unicode__ properly. + if not isinstance(result, unicode_type): + try: + result = unicode_type(some, self._sourceEncoding) + except Exception as error: + self._raiseStrOrUnicodeBroken("unicode", some, error) + return result + + def _isNameStartChar(self, some): + """ + NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] + | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] + | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] + | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] + """ + assert some + return True + + def _isNameChar(self, some): + """ + NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 + | [#x0300-#x036F] | [#x203F-#x2040] + """ + assert some + return True + + def _elementName(self, name, namespace): + assert name + if namespace: + result = "%s:%s" % (namespace, name) + else: + result = name + return result + + def _validateIsOpen(self): + if not self._isOpen: + raise XmlError("operation must be performed before writer is closed") + + def _validateNamespaceItem(self, itemName, namespace, qualifiedName): + if namespace: + namespaceFound = False + scopeIndex = self._scope() + while not namespaceFound and (scopeIndex >= 0): + namespacesForScope = self._namespaces.get(scopeIndex) + if namespacesForScope: + if namespace in [namespaceToCompareWith for namespaceToCompareWith, _ in namespacesForScope]: + namespaceFound = True + scopeIndex -= 1 + if not namespaceFound: + if namespace == "xmlns": + # TODO: raise XmlError("namespace '%s' must be added using `addNamespace()`.") + pass + else: + raise XmlError("namespace '%s' for %s '%s' must be added before use" % (namespace, itemName, qualifiedName)) + + def _write(self, text): + assert text is not None + _assertIsUnicode("text", text) + self._output.write(self._encoded(text)) + if not self._contentHasBeenWritten and text: + self._contentHasBeenWritten = True + + def _writeIndent(self): + self._write(self._indent * len(self._elementStack)) + + def _writePrettyIndent(self): + if self._pretty: + self._writeIndent() + + def _writePrettyNewline(self): + if self._pretty: + self.newline() + + def _writeEscaped(self, text): + assert text is not None + _assertIsUnicode("text", text) + self._write(xml.sax.saxutils.escape(text)) + + def newline(self): + self._possiblyFlushTag() + self._write(self._newline) + + def addNamespace(self, name, uri): + """ + Add namespace to the following elements by adding a ``xmlns`` + attribute to the next tag that is written using `startTag()` or `tag()`. + """ + # TODO: Validate that name is NCName. + _validateNotNoneOrEmpty("name", name) + _validateNotNoneOrEmpty("uri", uri) + uniName = self._unicodedFromString(name) + uniUri = self._unicodedFromString(uri) + namespacesForScope = self._namespaces.get(self._scope()) + namespaceExists = (uniName in self._namespacesToAdd) or ( + (namespacesForScope != None) and (uniName in namespacesForScope) + ) + if namespaceExists: + raise XmlError("namespace %r must added only once for current scope but already is %r" % (uniName, uniUri)) + self._namespacesToAdd.append((uniName, uniUri)) + + def _possiblyWriteTag(self, namespace, name, close, attributes={}): + _assertIsUnicode("namespace", namespace) + assert name + _assertIsUnicode("name", name) + assert close + assert close in (XmlWriter._CLOSE_NONE, XmlWriter._CLOSE_AT_START, XmlWriter._CLOSE_AT_END) + assert attributes is not None + + actualAttributes = {} + + # TODO: Validate that no "xmlns" attributes are specified by hand. + + # Process new namespaces to add. + if close in [XmlWriter._CLOSE_NONE, XmlWriter._CLOSE_AT_END]: + while self._namespacesToAdd: + namespaceName, uri = self._namespacesToAdd.pop() + if namespaceName: + actualAttributes["xmlns:%s" % namespaceName] = uri + else: + actualAttributes["xmlns"] = uri + namespacesForScope = self._namespaces.get(self._scope()) + if namespacesForScope == None: + namespacesForScope = [] + self._namespaces[self._scope()] = namespacesForScope + assert namespaceName not in [existingName for existingName, _ in namespacesForScope] + namespacesForScope.append((namespaceName, uri)) + self._namespaces[namespaceName] = uri + else: + if self._namespacesToAdd: + namespaceNames = ", ".join([name for name, _ in self._namespacesToAdd]) + raise XmlError("namespaces must be added before startTag() or tag(): %s" % namespaceNames) + + # Convert attributes to unicode. + for qualifiedAttributeName, attributeValue in list(attributes.items()): + uniQualifiedAttributeName = self._unicodedFromString(qualifiedAttributeName) + attributeNamespace, attributeName = _splitPossiblyQualifiedName("attribute name", uniQualifiedAttributeName) + self._validateNamespaceItem("attribute", attributeNamespace, attributeName) + actualAttributes[uniQualifiedAttributeName] = self._unicoded(attributeValue) + + # Prepare indentation and qualified tag name to be written. + if self.isPretty: + indent = self._indent * len(self._elementStack) + else: + indent = "" + self._validateNamespaceItem("tag", namespace, name) + if namespace: + qualifiedTagName = "%s:%s" % (namespace, name) + else: + qualifiedTagName = name + + if close == XmlWriter._CLOSE_NONE: + self._startTagToWrite = (indent, qualifiedTagName, actualAttributes) + else: + self._actuallyWriteTag(indent, qualifiedTagName, actualAttributes, close) + + # Process name spaces to remove + if close in [XmlWriter._CLOSE_AT_END, XmlWriter._CLOSE_AT_START]: + scopeToRemove = self._scope() + if scopeToRemove in self._namespaces: + del self._namespaces[scopeToRemove] + + def _actuallyWriteTag(self, indent, qualifiedTagName, attributes, close): + assert self._startTagToWrite is None + assert indent is not None + _assertIsUnicode("indent", indent) + assert qualifiedTagName + _assertIsUnicode("qualifiedTagName", qualifiedTagName) + assert close + assert close in (XmlWriter._CLOSE_NONE, XmlWriter._CLOSE_AT_START, XmlWriter._CLOSE_AT_END) + assert attributes is not None + if self._pretty: + self._write(indent) + self._write("<") + if close == XmlWriter._CLOSE_AT_START: + self._write("/") + self._write(qualifiedTagName) + for attributeName in sorted(attributes.keys()): + _assertIsUnicode("attribute name", attributeName) + value = attributes[attributeName] + _assertIsUnicode("value of attribute %r" % attributeName, value) + self._write(" %s=%s" % (attributeName, _quoted(value))) + if close == XmlWriter._CLOSE_AT_END: + if self.isPretty: + self._write(" ") + self._write("/") + self._write(">") + if self._pretty: + self.newline() + + def _possiblyFlushTag(self): + """ + If ``self._startTagToWrite`` is set, it contains a tuple + ``(indent, qualifiedTagName, attributes)`` describing a start tag that has not + been written yet. In this case, write the tag now and set + ``self._startTagToWrite`` to ``None``. This allows to optimize a sequence + of ``startTag()``/ ``endTag()`` with the same tag to be changed to + a simple ``tag()``. + """ + if self._startTagToWrite: + indent, qualifiedTagName, attributes = self._startTagToWrite; + self._startTagToWrite = None + self._actuallyWriteTag(indent, qualifiedTagName, attributes, XmlWriter._CLOSE_NONE) + + def startTag(self, qualifiedName, attributes={}): + """ + Start tag with name ``qualifiedName``, optionally using a namespace + prefix separated with a colon (:) and ``attributes``. + + Example names are "img" and "xhtml:img" (assuming the namespace prefix + "xtml" has been added before using `addNamespace()`). + + Attributes are a dictionary containing the attribute name and value, for + example:: + + {"src": "../some.png", "xhtml:alt": "some image"} + """ + self._possiblyFlushTag() + uniQualifiedName = self._unicodedFromString(qualifiedName) + namespace, name = _splitPossiblyQualifiedName("tag name", uniQualifiedName) + self._possiblyWriteTag(namespace, name, XmlWriter._CLOSE_NONE, attributes) + self._elementStack.append((namespace, name)) + + def endTag(self, expectedQualifiedName=None): + """ + End tag that has been started before using `startTag()`, + optionally checking that the name matches ``expectedQualifiedName``. + + As example, consider the following writer with a namespace: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out) + >>> xml.addNamespace("xhtml", "http://www.w3.org/1999/xhtml") + + Now start a couple of elements: + + >>> xml.startTag("html") + >>> xml.startTag("xhtml:body") + + Try to end a mistyped tag: + + >>> xml.endTag("xhtml:doby") + Traceback (most recent call last): + ... + XmlError: tag name must be xhtml:doby but is xhtml:body + + Try again properly: + + >>> xml.endTag("xhtml:body") + + Try to end another mistyped tag, this time without namespace: + + >>> xml.endTag("xml") + Traceback (most recent call last): + ... + XmlError: tag name must be xml but is html + + End the tag properly, this time without an expected name: + + >>> xml.endTag() + + Try to end another tag without any left: + + >>> xml.endTag() + Traceback (most recent call last): + ... + XmlError: tag stack must not be empty + """ + try: + (namespace, name) = self._elementStack.pop() + except IndexError: + raise XmlError("tag stack must not be empty") + actualQualifiedName = _joinPossiblyQualifiedName(namespace, name) + if expectedQualifiedName: + # Validate that actual tag name matches expected name. + uniExpectedQualifiedName = self._unicodedFromString(expectedQualifiedName) + if actualQualifiedName != expectedQualifiedName: + self._elementStack.append((namespace, name)) + raise XmlError("tag name must be %s but is %s" % (uniExpectedQualifiedName, actualQualifiedName)) + + isConsolidatableStartEndTag = False + if self._startTagToWrite: + _, qualifiedStartTagName, attributes = self._startTagToWrite + if actualQualifiedName == qualifiedStartTagName: + isConsolidatableStartEndTag = True + if isConsolidatableStartEndTag: + self._startTagToWrite = None + self._possiblyWriteTag(namespace, name, XmlWriter._CLOSE_AT_END, attributes) + else: + self._possiblyFlushTag() + self._possiblyWriteTag(namespace, name, XmlWriter._CLOSE_AT_START) + + + def endTags(self, count=0): + """ + End tags is useful if you need to close a couple of tags in one command + it might be useful when you need to close a few root tags. + + As example, consider the following writer with a namespace: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out) + >>> xml.addNamespace("xhtml", "http://www.w3.org/1999/xhtml") + + Now start a couple of elements: + + >>> xml.startTag("html") + >>> xml.startTag("xhtml:body") + >>> xml.startTag("xhtml:div") + >>> xml.startTag("xhtml:span") + >>> xml.startTag("xhtml:b") + + Try to end bad number of tags: + + >>> xml.endTags(7) # actually there are 5 tags, not 7 + Traceback (most recent call last): + ... + XmlError: cannot close 7 tags, 5 remaining + + Try to end 2 tags: + + >>> xml.endTags(2) + + And now finally end all started tags: + + >>> xml.endTags() + + But if all the tags closed it will raise the same error as endTag + + >>> xml.endTags() + Traceback (most recent call last): + ... + XmlError: tag stack must not be empty + """ + + stackLen = int(len(self._elementStack)) + + if stackLen == 0: + raise XmlError("tag stack must not be empty") + if count == 0: + count = stackLen + elif stackLen < count: + raise XmlError("cannot close %d tags," + " %d remaining" % (count, stackLen)) + + for _ in range(count): + self.endTag() + + def tag(self, qualifiedName, attributes={}): + self._possiblyFlushTag() + uniQualifiedName = self._unicodedFromString(qualifiedName) + namespace, name = _splitPossiblyQualifiedName("tag name", uniQualifiedName) + self._possiblyWriteTag(namespace, name, XmlWriter._CLOSE_AT_END, attributes) + + def text(self, text): + """ + Write ``text`` using escape sequences if needed. + + Using a writer like + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + + you can write some text: + + >>> xml.text(" & ") + >>> print out.getvalue().rstrip("\\r\\n") + <this> & <that> + + If ``text`` contains line feeds, the will be normalized to `newline()`: + + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + >>> xml.startTag("some") + >>> xml.text("a text\\nwith multiple lines\\n and indentation and trailing blanks ") + >>> xml.endTag() + >>> print out.getvalue().rstrip("\\r\\n") + + a text + with multiple lines + and indentation and trailing blanks + + + Empty text does not result in any output: + + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + >>> xml.startTag("some") + >>> xml.text("") + >>> xml.endTag() + >>> print out.getvalue().rstrip("\\r\\n") + + + """ + self._possiblyFlushTag() + _validateNotNone("text", text) + uniText = self._unicodedFromString(text) + if self._pretty: + for uniLine in io.StringIO(uniText): + self._writeIndent() + uniLine = uniLine.lstrip(" \t").rstrip(" \t\r\n") + self._writeEscaped(uniLine) + self.newline() + else: + self._writeEscaped(uniText) + + + def comment(self, text, embedInBlanks=True): + """ + Write an XML comment. + + As example set up a writer: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + + Now add the comment + + >>> xml.comment("some comment") + + And the result is: + + >>> print out.getvalue().rstrip("\\r\\n") + + + A comment can spawn multiple lines. If pretty is enabled, the lines + will be indented. Again, first set up a writer: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + + Then add the comment + + >>> xml.comment("some comment\\nspawning mutiple\\nlines") + + And the result is: + + >>> print out.getvalue().rstrip("\\r\\n") + + """ + self._possiblyFlushTag() + uniText = self._unicodedFromString(text) + if not embedInBlanks and not uniText: + raise XmlError("text for comment must not be empty, or option embedInBlanks=True must be set") + if "--" in uniText: + raise XmlError("text for comment must not contain \"--\"") + hasNewline = ("\n" in uniText) or ("\r" in uniText) + hasStartBlank = uniText and uniText[0].isspace() + hasEndBlank = (len(uniText) > 1) and uniText[-1].isspace() + self._writePrettyIndent() + self._write(""); + if self._pretty: + self.newline() + + + def cdata(self, text): + """ + Write a CDATA section. + + As example set up a writer: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + + Now add the CDATA section: + + >>> xml.cdata("some data\\nlines\\n&&&") + + And the result is: + + >>> print out.getvalue().rstrip("\\r\\n") + &&&]]> + """ + self._possiblyFlushTag() + self._rawBlock("CDATA section", XmlWriter._CDATA_START, XmlWriter._CDATA_END, text) + + def processingInstruction(self, target, text): + """ + Write a processing instruction. + + As example set up a writer: + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + + Now add the processing instruction: + + >>> xml.processingInstruction("xsl-stylesheet", "href=\\"some.xsl\\" type=\\"text/xml\\"") + + And the result is: + + >>> print out.getvalue().rstrip("\\r\\n") + + """ + self._possiblyFlushTag() + targetName = "target for processing instrution" + _validateNotNone(targetName, text) + _validateNotEmpty(targetName, text) + uniFullText = self._unicodedFromString(target) + if text: + uniFullText += " " + uniFullText += self._unicodedFromString(text) + self._rawBlock("processing instruction", XmlWriter._PROCESSING_START, XmlWriter._PROCESSING_END, uniFullText) + + def _rawBlock(self, name, start, end, text): + _assertIsUnicode("name", name) + _assertIsUnicode("start", start) + _assertIsUnicode("end", end) + _validateNotNone("text for %s" % name, text) + uniText = self._unicodedFromString(text) + if end in uniText: + raise XmlError("text for %s must not contain \"%s\"" % (name, end)) + self._writePrettyIndent() + self._write(start) + self._write(uniText) + self._write(end) + self._writePrettyNewline() + + def raw(self, text): + """ + Write raw ``text`` without escaping, validation and pretty printing. + + Using a writer like + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + + you can use ``raw`` for good and add for exmaple a doctype declaration: + + >>> xml.raw("") + >>> print out.getvalue().rstrip("\\r\\n") + + + but you can also do all sorts of evil things which can invalidate the XML document: + + >>> out = io.BytesIO() + >>> xml = XmlWriter(out, prolog=False) + >>> xml.raw(">(^_^)< not particular valid XML &&&") + >>> print out.getvalue().rstrip("\\r\\n") + >(^_^)< not particular valid XML &&& + """ + self._possiblyFlushTag() + _validateNotNone("text", text) + uniText = self._unicodedFromString(text) + self._write(uniText) + + def close(self): + """ + Close the writer, validate that all started elements have ended and + prevent further output. + + Using a writer like + + >>> import io + >>> out = io.BytesIO() + >>> xml = XmlWriter(out) + + you can write a tag without closing it: + + >>> xml.startTag("some") + + However, once you try to close the writer, you get: + + >>> xml.close() + Traceback (most recent call last): + ... + XmlError: missing end tags must be added: + """ + self._possiblyFlushTag() + remainingElements = "" + while self._elementStack: + if remainingElements: + remainingElements += ", " + namespace, name = self._elementStack.pop() + remainingElements += "" % self._elementName(name, namespace) + if remainingElements: + raise XmlError("missing end tags must be added: %s" % remainingElements) + + +class ChainXmlWriter(XmlWriter): + """ + XmlWriter-wrapper for method chaining, here is an example: + >>> import io + >>> out = io.BytesIO() + >>> xml = ChainXmlWriter(out) + >>> xml.addNamespace("xhtml", "http://www.w3.org/1999/xhtml") #doctest: +ELLIPSIS + + >>> xml.startTag("xhtml:html").startTag("xhtml:body") #doctest: +ELLIPSIS + + >>> xml.text("Hello world!").tag("xhtml:img", {"src": "smile.png", "alt": ":-)"}) #doctest: +ELLIPSIS + + >>> xml.endTags().close() + + And the result is: + + >>> print out.getvalue().rstrip("\\r\\n") + + + + Hello world! + + + + """ + + chainableMethods = ('addNamespace', 'cdata', 'comment', 'endTag', + 'endTags', 'processingInstruction', 'startTag', 'tag', + 'text',) + + def _chainDecorator(self, func): + def _wrapper(*args, **kwargs): + func(*args, **kwargs) + return self + return _wrapper + + def __getattribute__(self, name): + + if name in ChainXmlWriter.chainableMethods: + originalMethod = getattr(super(ChainXmlWriter, self), name) + return self._chainDecorator(originalMethod) + + return super(ChainXmlWriter, self).__getattribute__(name) + +if __name__ == "__main__": + import doctest + print("loxun %s: running doctest" % __version__) + doctest.testmod() + diff --git a/tablib/packages/ods.py b/tablib/packages/ods.py new file mode 100644 index 00000000..bcbd04ba --- /dev/null +++ b/tablib/packages/ods.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2005-2016 Entr'ouvert +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + +import tempfile +import zipfile + +from tablib.packages import loxun +from tablib.compat import unicode + +OFFICE_NS = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0' +TABLE_NS = 'urn:oasis:names:tc:opendocument:xmlns:table:1.0' +TEXT_NS = 'urn:oasis:names:tc:opendocument:xmlns:text:1.0' +XLINK_NS = 'http://www.w3.org/1999/xlink' +STYLE_NS = 'urn:oasis:names:tc:opendocument:xmlns:style:1.0' + + +class ODSWorkbook(object): + OPENED = 1 + CLOSED = 2 + INSHEET = 3 + INROW = 4 + + def __init__(self, output): + z = self.z = zipfile.ZipFile(output, 'w') + z.writestr('mimetype', 'application/vnd.oasis.opendocument.spreadsheet') + z.writestr('META-INF/manifest.xml', ''' + + + + + + +''') + z.writestr('styles.xml', ''' + +''') + self.content = tempfile.NamedTemporaryFile() + xml = self.xmlwriter = loxun.XmlWriter(self.content, pretty=False) + xml.addNamespace('office', OFFICE_NS) + xml.addNamespace('style', STYLE_NS) + xml.addNamespace('table', TABLE_NS) + xml.addNamespace('xlink', XLINK_NS) + xml.addNamespace('text', TEXT_NS) + # add bold style for headers + xml.startTag('office:document-content') + xml.startTag('office:automatic-styles') + xml.startTag('style:style', { + 'style:family': 'paragraph', + 'style:name': 'bold', + 'style:display-name': 'bold', + }) + xml.tag('style:text-properties', { + 'style:font-weight-complex': 'bold', + 'style:font-weight': 'bold', + 'style:font-weight-asian': 'bold' + }) + xml.endTag() + xml.endTag() + xml.startTag('office:body') + xml.startTag('office:spreadsheet') + self.status = self.OPENED + + def close(self): + assert self.status == self.OPENED + self.status = self.CLOSED + xml = self.xmlwriter + xml.endTag() + xml.endTag() + xml.endTag() + self.z.write(self.content.name, 'content.xml') + self.content.close() + self.z.close() + del self.z + del self.xmlwriter + del self.content + + def start_sheet(self, columns, title=None): + assert self.status == self.OPENED + self.status = self.INSHEET + xml = self.xmlwriter + attribs = {} + if title: + attribs['table:name'] = title + xml.startTag('table:table', attribs) + for i in range(columns): + xml.tag('table:table-column') + + def end_sheet(self): + assert self.status == self.INSHEET + self.status = self.OPENED + self.xmlwriter.endTag() + + def add_headers(self, headers): + self.add_row(headers, { + 'table:style-name': 'bold', + 'table:default-cell-style-name': 'bold', + }, hint='header') + + def add_row(self, row, attribs={}, hint=None): + self.start_row(attribs) + for cell in row: + self.add_cell(cell, hint=hint) + self.end_row() + + def start_row(self, attribs={}): + assert self.status == self.INSHEET + self.status = self.INROW + self.xmlwriter.startTag('table:table-row', attribs) + + def end_row(self): + assert self.status == self.INROW + self.status = self.INSHEET + self.xmlwriter.endTag() + + def add_cell(self, content, hint=None): + assert self.status == self.INROW + content = unicode(content) + self.xmlwriter.startTag('table:table-cell', { + 'office:value-type': 'string', + }) + self.xmlwriter.startTag('text:p') + attribs = {} + if hint == 'header': + attribs['text:style-name'] = 'bold' + self.xmlwriter.startTag('text:span', attribs) + self.xmlwriter.text(content) + self.xmlwriter.endTag() + self.xmlwriter.endTag() + self.xmlwriter.endTag() + + def __del__(self): + if getattr(self, 'content', None) is not None: + try: + self.content.close() + except: + pass