summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKenneth Reitz <me@kennethreitz.com>2011-05-13 01:15:06 -0400
committerKenneth Reitz <me@kennethreitz.com>2011-05-13 01:15:06 -0400
commit9146de36d45d2a6bc9adb0375bcc186d9b7167be (patch)
tree06cb94b5709211c132c41699af70fbc328b670d1
parent865ce6278296cb0998e5544bdfa4e4e8983d7bb2 (diff)
parent9761ff5e9ef3bed0cd4c7e9a00e5c10bd453e6c7 (diff)
downloadtablib-9146de36d45d2a6bc9adb0375bcc186d9b7167be.tar.gz
Merge branch 'release/0.9.7'v0.9.7
-rw-r--r--AUTHORS3
-rw-r--r--HISTORY.rst8
-rw-r--r--docs/conf.py2
-rw-r--r--setup.py3
-rw-r--r--tablib/__init__.py16
-rw-r--r--tablib/compat.py49
-rw-r--r--tablib/core.py31
-rw-r--r--tablib/core25.py818
-rw-r--r--tablib/formats/__init__.py3
-rw-r--r--tablib/formats/_xls.py10
-rw-r--r--tablib/formats/_xlsx.py101
-rw-r--r--tablib/packages/openpyxl/__init__.py53
-rw-r--r--tablib/packages/openpyxl/cell.py384
-rw-r--r--tablib/packages/openpyxl/chart.py340
-rw-r--r--tablib/packages/openpyxl/drawing.py401
-rw-r--r--tablib/packages/openpyxl/namedrange.py68
-rw-r--r--tablib/packages/openpyxl/reader/__init__.py33
-rw-r--r--tablib/packages/openpyxl/reader/excel.py109
-rw-r--r--tablib/packages/openpyxl/reader/iter_worksheet.py348
-rw-r--r--tablib/packages/openpyxl/reader/strings.py64
-rw-r--r--tablib/packages/openpyxl/reader/style.py69
-rw-r--r--tablib/packages/openpyxl/reader/workbook.py156
-rw-r--r--tablib/packages/openpyxl/reader/worksheet.py114
-rw-r--r--tablib/packages/openpyxl/shared/__init__.py33
-rw-r--r--tablib/packages/openpyxl/shared/date_time.py154
-rw-r--r--tablib/packages/openpyxl/shared/exc.py59
-rw-r--r--tablib/packages/openpyxl/shared/ooxml.py60
-rw-r--r--tablib/packages/openpyxl/shared/password_hasher.py47
-rw-r--r--tablib/packages/openpyxl/shared/units.py67
-rw-r--r--tablib/packages/openpyxl/shared/xmltools.py114
-rw-r--r--tablib/packages/openpyxl/style.py392
-rw-r--r--tablib/packages/openpyxl/workbook.py186
-rw-r--r--tablib/packages/openpyxl/worksheet.py534
-rw-r--r--tablib/packages/openpyxl/writer/__init__.py34
-rw-r--r--tablib/packages/openpyxl/writer/charts.py261
-rw-r--r--tablib/packages/openpyxl/writer/drawings.py192
-rw-r--r--tablib/packages/openpyxl/writer/dump_worksheet.py256
-rw-r--r--tablib/packages/openpyxl/writer/excel.py161
-rw-r--r--tablib/packages/openpyxl/writer/strings.py86
-rw-r--r--tablib/packages/openpyxl/writer/styles.py256
-rw-r--r--tablib/packages/openpyxl/writer/theme.py202
-rw-r--r--tablib/packages/openpyxl/writer/workbook.py204
-rw-r--r--tablib/packages/openpyxl/writer/worksheet.py209
-rw-r--r--tablib/packages/openpyxl3/__init__.py53
-rw-r--r--tablib/packages/openpyxl3/cell.py384
-rw-r--r--tablib/packages/openpyxl3/chart.py340
-rw-r--r--tablib/packages/openpyxl3/drawing.py402
-rw-r--r--tablib/packages/openpyxl3/namedrange.py68
-rw-r--r--tablib/packages/openpyxl3/reader/__init__.py33
-rw-r--r--tablib/packages/openpyxl3/reader/excel.py117
-rw-r--r--tablib/packages/openpyxl3/reader/iter_worksheet.py343
-rw-r--r--tablib/packages/openpyxl3/reader/strings.py64
-rw-r--r--tablib/packages/openpyxl3/reader/style.py69
-rw-r--r--tablib/packages/openpyxl3/reader/workbook.py156
-rw-r--r--tablib/packages/openpyxl3/reader/worksheet.py117
-rw-r--r--tablib/packages/openpyxl3/shared/__init__.py33
-rw-r--r--tablib/packages/openpyxl3/shared/date_time.py154
-rw-r--r--tablib/packages/openpyxl3/shared/exc.py59
-rw-r--r--tablib/packages/openpyxl3/shared/ooxml.py60
-rw-r--r--tablib/packages/openpyxl3/shared/password_hasher.py47
-rw-r--r--tablib/packages/openpyxl3/shared/units.py67
-rw-r--r--tablib/packages/openpyxl3/shared/xmltools.py96
-rw-r--r--tablib/packages/openpyxl3/style.py392
-rw-r--r--tablib/packages/openpyxl3/workbook.py186
-rw-r--r--tablib/packages/openpyxl3/worksheet.py534
-rw-r--r--tablib/packages/openpyxl3/writer/__init__.py34
-rw-r--r--tablib/packages/openpyxl3/writer/charts.py262
-rw-r--r--tablib/packages/openpyxl3/writer/drawings.py192
-rw-r--r--tablib/packages/openpyxl3/writer/dump_worksheet.py256
-rw-r--r--tablib/packages/openpyxl3/writer/excel.py156
-rw-r--r--tablib/packages/openpyxl3/writer/strings.py86
-rw-r--r--tablib/packages/openpyxl3/writer/styles.py256
-rw-r--r--tablib/packages/openpyxl3/writer/theme.py202
-rw-r--r--tablib/packages/openpyxl3/writer/workbook.py204
-rw-r--r--tablib/packages/openpyxl3/writer/worksheet.py209
-rwxr-xr-xtest_tablib.py17
-rwxr-xr-xtest_tablib.py.orig522
77 files changed, 12000 insertions, 860 deletions
diff --git a/AUTHORS b/AUTHORS
index 1ca2018..facbd2e 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -14,4 +14,5 @@ Patches and Suggestions
- Josh Ourisman
- Luca Beltrame
- Benjamin Wohlwend
-- Erik Youngren \ No newline at end of file
+- Erik Youngren
+- Mark Rogers \ No newline at end of file
diff --git a/HISTORY.rst b/HISTORY.rst
index f545a4a..2f04060 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,6 +1,14 @@
History
-------
+0.9.7 (2011-05-12)
+++++++++++++++++++
+
+* Full XLSX Support!
+* Pickling Bugfix
+* Compat Module
+
+
0.9.6 (2011-05-12)
++++++++++++++++++
diff --git a/docs/conf.py b/docs/conf.py
index 01f4933..5a42816 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -48,7 +48,7 @@ copyright = u'2011, Kenneth Reitz. Styles (modified) &copy; Armin Ronacher'
# built documents.
#
# The short X.Y version.
-version = '0.9.6'
+version = '0.9.7'
# The full version, including alpha/beta/rc tags.
release = version
diff --git a/setup.py b/setup.py
index bb7ef54..307307f 100644
--- a/setup.py
+++ b/setup.py
@@ -22,7 +22,7 @@ if sys.version_info[:2] < (2,6):
setup(
name='tablib',
- version='0.9.6',
+ version='0.9.7',
description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)',
long_description=open('README.rst').read() + '\n\n' +
open('HISTORY.rst').read(),
@@ -33,6 +33,7 @@ setup(
'tablib', 'tablib.formats',
'tablib.packages',
'tablib.packages.xlwt',
+ 'tablib.packages.openpyxl',
'tablib.packages.yaml',
'tablib.packages.unicodecsv'
],
diff --git a/tablib/__init__.py b/tablib/__init__.py
index dc85527..c7ae7c0 100644
--- a/tablib/__init__.py
+++ b/tablib/__init__.py
@@ -1,16 +1,8 @@
""" Tablib.
"""
-import sys
-if sys.version_info[0:1] > (2, 5):
- from tablib.core import (
- Databook, Dataset, detect, import_set,
- InvalidDatasetType, InvalidDimensions, UnsupportedFormat
- )
-
-else:
- from tablib.core25 import (
- Databook, Dataset, detect, import_set,
- InvalidDatasetType, InvalidDimensions, UnsupportedFormat
- )
+from tablib.core import (
+ Databook, Dataset, detect, import_set,
+ InvalidDatasetType, InvalidDimensions, UnsupportedFormat
+)
diff --git a/tablib/compat.py b/tablib/compat.py
new file mode 100644
index 0000000..48e0081
--- /dev/null
+++ b/tablib/compat.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+"""
+tablib.compat
+~~~~~~~~~~~~~
+
+Tablib compatiblity module.
+
+"""
+
+import sys
+
+is_py3 = (sys.version_info[0] > 2)
+
+
+
+try:
+ from collections import OrderedDict
+except ImportError:
+ from tablib.packages.ordereddict import OrderedDict
+
+
+if is_py3:
+ from io import BytesIO
+ import tablib.packages.xlwt3 as xlwt
+ from tablib.packages import markup3 as markup
+ from tablib.packages import openpyxl3 as openpyxl
+
+ # py3 mappings
+ ifilter = filter
+ xrange = range
+ unicode = str
+ bytes = bytes
+ basestring = str
+
+else:
+ from cStringIO import StringIO as BytesIO
+ import tablib.packages.xlwt as xlwt
+ from tablib.packages import markup
+ from itertools import ifilter
+ from tablib.packages import openpyxl
+
+ # py2 mappings
+ xrange = xrange
+ unicode = unicode
+ bytes = str
+ basestring = basestring
+
+
diff --git a/tablib/core.py b/tablib/core.py
index 896dfcc..03547ff 100644
--- a/tablib/core.py
+++ b/tablib/core.py
@@ -13,17 +13,14 @@ from copy import copy
from operator import itemgetter
from tablib import formats
-import collections
-try:
- from collections import OrderedDict
-except ImportError:
- from tablib.packages.ordereddict import OrderedDict
+
+from tablib.compat import OrderedDict
__title__ = 'tablib'
-__version__ = '0.9.4'
-__build__ = 0x000904
+__version__ = '0.9.7'
+__build__ = 0x000907
__author__ = 'Kenneth Reitz'
__license__ = 'MIT'
__copyright__ = 'Copyright 2011 Kenneth Reitz'
@@ -278,7 +275,8 @@ class Dataset(object):
else:
header = []
- if len(col) == 1 and isinstance(col[0], collections.Callable):
+ if len(col) == 1 and hasattr(col[0], '__call__'):
+
col = list(map(col[0], self._data))
col = tuple(header + col)
@@ -390,6 +388,19 @@ class Dataset(object):
"""
pass
+ @property
+ def xlsx():
+ """An Excel Spreadsheet representation of the :class:`Dataset` object, with :ref:`separators`. Cannot be set.
+
+ .. admonition:: Binary Warning
+
+ :class:`Dataset.xlsx` contains binary data, so make sure to write in binary mode::
+
+ with open('output.xlsx', 'wb') as f:
+ f.write(data.xlsx)'
+ """
+ pass
+
@property
def csv():
@@ -549,7 +560,7 @@ class Dataset(object):
col = list(col)
# Callable Columns...
- if len(col) == 1 and isinstance(col[0], collections.Callable):
+ if len(col) == 1 and hasattr(col[0], '__call__'):
col = list(map(col[0], self._data))
col = self._clean_col(col)
@@ -795,7 +806,7 @@ def import_set(stream):
format.import_set(data, stream)
return data
- except AttributeError as e:
+ except AttributeError:
return None
diff --git a/tablib/core25.py b/tablib/core25.py
deleted file mode 100644
index c8352a6..0000000
--- a/tablib/core25.py
+++ /dev/null
@@ -1,818 +0,0 @@
-# -*- coding: utf-8 -*-
-u"""
- tablib.core
- ~~~~~~~~~~~
-
- This module implements the central Tablib objects.
-
- :copyright: (c) 2011 by Kenneth Reitz.
- :license: MIT, see LICENSE for more details.
-"""
-
-from copy import copy
-from operator import itemgetter
-
-from tablib import formats
-import collections
-from itertools import izip
-from itertools import imap
-
-try:
- from collections import OrderedDict
-except ImportError:
- from tablib.packages.ordereddict import OrderedDict
-
-
-__title__ = u'tablib'
-__version__ = u'0.9.4'
-__build__ = 0x000904
-__author__ = u'Kenneth Reitz'
-__license__ = u'MIT'
-__copyright__ = u'Copyright 2011 Kenneth Reitz'
-__docformat__ = u'restructuredtext'
-
-
-class Row(object):
- u"""Internal Row object. Mainly used for filtering."""
-
- __slots__ = [u'tuple', u'_row', u'tags']
-
- def __init__(self, row=list(), tags=list()):
- self._row = list(row)
- self.tags = list(tags)
-
- def __iter__(self):
- return (col for col in self._row)
-
- def __len__(self):
- return len(self._row)
-
- def __repr__(self):
- return repr(self._row)
-
- def __getslice__(self, i, j):
- return self._row[i,j]
-
- def __getitem__(self, i):
- return self._row[i]
-
- def __setitem__(self, i, value):
- self._row[i] = value
-
- def __delitem__(self, i):
- del self._row[i]
-
- def __getstate__(self):
- return {slot: [getattr(self, slot) for slot in self.__slots__]}
-
- def __setstate__(self, state):
- for (k, v) in list(state.items()): setattr(self, k, v)
-
- def append(self, value):
- self._row.append(value)
-
- def insert(self, index, value):
- self._row.insert(index, value)
-
- def __contains__(self, item):
- return (item in self._row)
-
- @property
- def tuple(self):
- u'''Tuple representation of :class:`Row`.'''
- return tuple(self._row)
-
- @property
- def list(self):
- u'''List representation of :class:`Row`.'''
- return list(self._row)
-
- def has_tag(self, tag):
- u"""Returns true if current row contains tag."""
-
- if tag == None:
- return False
- elif isinstance(tag, basestring):
- return (tag in self.tags)
- else:
- return bool(len(set(tag) & set(self.tags)))
-
-
-
-
-class Dataset(object):
- u"""The :class:`Dataset` object is the heart of Tablib. It provides all core
- functionality.
-
- Usually you create a :class:`Dataset` instance in your main module, and append
- rows and columns as you collect data. ::
-
- data = tablib.Dataset()
- data.headers = ('name', 'age')
-
- for (name, age) in some_collector():
- data.append((name, age))
-
- You can also set rows and headers upon instantiation. This is useful if dealing
- with dozens or hundres of :class:`Dataset` objects. ::
-
- headers = ('first_name', 'last_name')
- data = [('John', 'Adams'), ('George', 'Washington')]
-
- data = tablib.Dataset(*data, headers=headers)
-
-
- :param \*args: (optional) list of rows to populate Dataset
- :param headers: (optional) list strings for Dataset header row
-
-
- .. admonition:: Format Attributes Definition
-
- If you look at the code, the various output/import formats are not
- defined within the :class:`Dataset` object. To add support for a new format, see
- :ref:`Adding New Formats <newformats>`.
-
- """
-
- def __init__(self, *args, **kwargs):
- self._data = list(Row(arg) for arg in args)
- self.__headers = None
-
- # ('title', index) tuples
- self._separators = []
-
- # (column, callback) tuples
- self._formatters = []
-
- try:
- self.headers = kwargs[u'headers']
- except KeyError:
- self.headers = None
-
- try:
- self.title = kwargs[u'title']
- except KeyError:
- self.title = None
-
- self._register_formats()
-
-
- def __len__(self):
- return self.height
-
-
- def __getitem__(self, key):
- if isinstance(key, basestring):
- if key in self.headers:
- pos = self.headers.index(key) # get 'key' index from each data
- return [row[pos] for row in self._data]
- else:
- raise KeyError
- else:
- _results = self._data[key]
- if isinstance(_results, Row):
- return _results.tuple
- else:
- return [result.tuple for result in _results]
-
-
- def __setitem__(self, key, value):
- self._validate(value)
- self._data[key] = Row(value)
-
-
- def __delitem__(self, key):
- if isinstance(key, basestring):
-
- if key in self.headers:
-
- pos = self.headers.index(key)
- del self.headers[pos]
-
- for i, row in enumerate(self._data):
-
- del row[pos]
- self._data[i] = row
- else:
- raise KeyError
- else:
- del self._data[key]
-
-
- def __repr__(self):
- try:
- return u'<%s dataset>' % (self.title.lower())
- except AttributeError:
- return u'<dataset object>'
-
-
- @classmethod
- def _register_formats(cls):
- u"""Adds format properties."""
- for fmt in formats.available:
- try:
- try:
- setattr(cls, fmt.title, property(fmt.export_set, fmt.import_set))
- except AttributeError:
- setattr(cls, fmt.title, property(fmt.export_set))
-
- except AttributeError:
- pass
-
-
- def _validate(self, row=None, col=None, safety=False):
- u"""Assures size of every row in dataset is of proper proportions."""
- if row:
- is_valid = (len(row) == self.width) if self.width else True
- elif col:
- if len(col) < 1:
- is_valid = True
- else:
- is_valid = (len(col) == self.height) if self.height else True
- else:
- is_valid = all((len(x) == self.width for x in self._data))
-
- if is_valid:
- return True
- else:
- if not safety:
- raise InvalidDimensions
- return False
-
-
- def _package(self, dicts=True):
- u"""Packages Dataset into lists of dictionaries for transmission."""
-
- _data = list(self._data)
-
- # Execute formatters
- if self._formatters:
- for row_i, row in enumerate(_data):
- for col, callback in self._formatters:
- try:
- if col is None:
- for j, c in enumerate(row):
- _data[row_i][j] = callback(c)
- else:
- _data[row_i][col] = callback(row[col])
- except IndexError:
- raise InvalidDatasetIndex
-
-
- if self.headers:
- if dicts:
- data = [OrderedDict(list(izip(self.headers, data_row))) for data_row in _data]
- else:
- data = [list(self.headers)] + list(_data)
- else:
- data = [list(row) for row in _data]
-
- return data
-
-
- def _clean_col(self, col):
- u"""Prepares the given column for insert/append."""
-
- col = list(col)
-
- if self.headers:
- header = [col.pop(0)]
- else:
- header = []
-
- if len(col) == 1 and hasattr(col[0], '__call__'):
- col = list(imap(col[0], self._data))
- col = tuple(header + col)
-
- return col
-
-
- @property
- def height(self):
- u"""The number of rows currently in the :class:`Dataset`.
- Cannot be directly modified.
- """
- return len(self._data)
-
-
- @property
- def width(self):
- u"""The number of columns currently in the :class:`Dataset`.
- Cannot be directly modified.
- """
-
- try:
- return len(self._data[0])
- except IndexError:
- try:
- return len(self.headers)
- except TypeError:
- return 0
-
-
- def _get_headers(self):
- u"""An *optional* list of strings to be used for header rows and attribute names.
-
- This must be set manually. The given list length must equal :class:`Dataset.width`.
-
- """
- return self.__headers
-
-
- def _set_headers(self, collection):
- u"""Validating headers setter."""
- self._validate(collection)
- if collection:
- try:
- self.__headers = list(collection)
- except TypeError:
- raise TypeError
- else:
- self.__headers = None
-
- headers = property(_get_headers, _set_headers)
-
- def _get_dict(self):
- u"""A native Python representation of the :class:`Dataset` object. If headers have
- been set, a list of Python dictionaries will be returned. If no headers have been set,
- a list of tuples (rows) will be returned instead.
-
- A dataset object can also be imported by setting the `Dataset.dict` attribute: ::
-
- data = tablib.Dataset()
- data.json = '[{"last_name": "Adams","age": 90,"first_name": "John"}]'
-
- """
- return self._package()
-
-
- def _set_dict(self, pickle):
- u"""A native Python representation of the Dataset object. If headers have been
- set, a list of Python dictionaries will be returned. If no headers have been
- set, a list of tuples (rows) will be returned instead.
-
- A dataset object can also be imported by setting the :class:`Dataset.dict` attribute. ::
-
- data = tablib.Dataset()
- data.dict = [{'age': 90, 'first_name': 'Kenneth', 'last_name': 'Reitz'}]
-
- """
-
- if not len(pickle):
- return
-
- # if list of rows
- if isinstance(pickle[0], list):
- self.wipe()
- for row in pickle:
- self.append(Row(row))
-
- # if list of objects
- elif isinstance(pickle[0], dict):
- self.wipe()
- self.headers = list(pickle[0].keys())
- for row in pickle:
- self.append(Row(list(row.values())))
- else:
- raise UnsupportedFormat
-
- dict = property(_get_dict, _set_dict)
-
-
- @property
- def xls():
- u"""An Excel Spreadsheet representation of the :class:`Dataset` object, with :ref:`seperators`. Cannot be set.
-
- .. admonition:: Binary Warning
-
- :class:`Dataset.xls` contains binary data, so make sure to write in binary mode::
-
- with open('output.xls', 'wb') as f:
- f.write(data.xls)'
- """
- pass
-
-
- @property
- def csv():
- u"""A CSV representation of the :class:`Dataset` object. The top row will contain
- headers, if they have been set. Otherwise, the top row will contain
- the first row of the dataset.
-
- A dataset object can also be imported by setting the :class:`Dataset.csv` attribute. ::
-
- data = tablib.Dataset()
- data.csv = 'age, first_name, last_name\\n90, John, Adams'
-
- Import assumes (for now) that headers exist.
- """
- pass
-
-
- @property
- def tsv():
- u"""A TSV representation of the :class:`Dataset` object. The top row will contain
- headers, if they have been set. Otherwise, the top row will contain
- the first row of the dataset.
-
- A dataset object can also be imported by setting the :class:`Dataset.tsv` attribute. ::
-
- data = tablib.Dataset()
- data.tsv = 'age\tfirst_name\tlast_name\\n90\tJohn\tAdams'
-
- Import assumes (for now) that headers exist.
- """
-
- @property
- def yaml():
- u"""A YAML representation of the :class:`Dataset` object. If headers have been
- set, a YAML list of objects will be returned. If no headers have
- been set, a YAML list of lists (rows) will be returned instead.
-
- A dataset object can also be imported by setting the :class:`Dataset.json` attribute: ::
-
- data = tablib.Dataset()
- data.yaml = '- {age: 90, first_name: John, last_name: Adams}'
-
- Import assumes (for now) that headers exist.
- """
- pass
-
-
- @property
- def json():
- u"""A JSON representation of the :class:`Dataset` object. If headers have been
- set, a JSON list of objects will be returned. If no headers have
- been set, a JSON list of lists (rows) will be returned instead.
-
- A dataset object can also be imported by setting the :class:`Dataset.json` attribute: ::
-
- data = tablib.Dataset()
- data.json = '[{age: 90, first_name: "John", liast_name: "Adams"}]'
-
- Import assumes (for now) that headers exist.
- """
-
- @property
- def html():
- u"""A HTML table representation of the :class:`Dataset` object. If
- headers have been set, they will be used as table headers.
-
- ..notice:: This method can be used for export only.
- """
- pass
-
-
- def append(self, row=None, col=None, header=None, tags=list()):
- u"""Adds a row or column to the :class:`Dataset`.
- Usage is :class:`Dataset.insert` for documentation.
- """
-
- if row is not None:
- self.insert(self.height, row=row, tags=tags)
- elif col is not None:
- self.insert(self.width, col=col, header=header)
-
-
- def insert_separator(self, index, text=u'-'):
- u"""Adds a separator to :class:`Dataset` at given index."""
-
- sep = (index, text)
- self._separators.append(sep)
-
-
- def append_separator(self, text=u'-'):
- u"""Adds a :ref:`seperator <seperators>` to the :class:`Dataset`."""
-
- # change offsets if headers are or aren't defined
- if not self.headers:
- index = self.height if self.height else 0
- else:
- index = (self.height + 1) if self.height else 1
-
- self.insert_separator(index, text)
-
-
- def add_formatter(self, col, handler):
- u"""Adds a :ref:`formatter` to the :class:`Dataset`.
-
- .. versionadded:: 0.9.5
- :param col: column to. Accepts index int or header str.
- :param handler: reference to callback function to execute
- against each cell value.
- """
-
- if isinstance(col, basestring):
- if col in self.headers:
- col = self.headers.index(col) # get 'key' index from each data
- else:
- raise KeyError
-
- if not col > self.width:
- self._formatters.append((col, handler))
- else:
- raise InvalidDatasetIndex
-
- return True
-
-
- def insert(self, index, row=None, col=None, header=None, tags=list()):
- u"""Inserts a row or column to the :class:`Dataset` at the given index.
-
- Rows and columns inserted must be the correct size (height or width).
-
- The default behaviour is to insert the given row to the :class:`Dataset`
- object at the given index. If the ``col`` parameter is given, however,
- a new column will be insert to the :class:`Dataset` object instead.
-
- You can also insert a column of a single callable object, which will
- add a new column with the return values of the callable each as an
- item in the column. ::
-
- data.append(col=random.randint)
-
- See :ref:`dyncols` for an in-depth example.
-
- .. versionchanged:: 0.9.0
- If inserting a column, and :class:`Dataset.headers` is set, the
- header attribute must be set, and will be considered the header for
- that row.
-
- .. versionadded:: 0.9.0
- If inserting a row, you can add :ref:`tags <tags>` to the row you are inserting.
- This gives you the ability to :class:`filter <Dataset.filter>` your
- :class:`Dataset` later.
-
- """
- if row:
- self._validate(row)
- self._data.insert(index, Row(row, tags=tags))
- elif col:
- col = list(col)
-
- # Callable Columns...
- if len(col) == 1 and hasattr(col[0], '__call__'):
- col = list(imap(col[0], self._data))
-
- col = self._clean_col(col)
- self._validate(col=col)
-
- if self.headers:
- # pop the first item off, add to headers
- if not header:
- raise HeadersNeeded()
- self.headers.insert(index, header)
-
- if self.height and self.width:
-
- for i, row in enumerate(self._data):
-
- row.insert(index, col[i])
- self._data[i] = row
- else:
- self._data = [Row([row]) for row in col]
-
-
- def filter(self, tag):
- u"""Returns a new instance of the :class:`Dataset`, excluding any rows
- that do not contain the given :ref:`tags <tags>`.
- """
- _dset = copy(self)
- _dset._data = [row for row in _dset._data if row.has_tag(tag)]
-
- return _dset
-
-
- def sort(self, col, reverse=False):
- u"""Sort a :class:`Dataset` by a specific column, given string (for
- header) or integer (for column index). The order can be reversed by
- setting ``reverse`` to ``True``.
- Returns a new :class:`Dataset` instance where columns have been
- sorted."""
-
- if isinstance(col, basestring):
-
- if not self.headers:
- raise HeadersNeeded
-
- _sorted = sorted(self.dict, key=itemgetter(col), reverse=reverse)
- _dset = Dataset(headers=self.headers)
-
- for item in _sorted:
- row = [item[key] for key in self.headers]
- _dset.append(row=row)
-
- else:
- if self.headers:
- col = self.headers[col]
-
- _sorted = sorted(self.dict, key=itemgetter(col), reverse=reverse)
- _dset = Dataset(headers=self.headers)
-
- for item in _sorted:
- if self.headers:
- row = [item[key] for key in self.headers]
- else:
- row = item
- _dset.append(row=row)
-
-
- return _dset
-
-
- def transpose(self):
- u"""Transpose a :class:`Dataset`, turning rows into columns and vice
- versa, returning a new ``Dataset`` instance. The first row of the
- original instance becomes the new header row."""
-
- # Don't transpose if there is no data
- if not self:
- return
-
- _dset = Dataset()
- # The first element of the headers stays in the headers,
- # it is our "hinge" on which we rotate the data
- new_headers = [self.headers[0]] + self[self.headers[0]]
-
- _dset.headers = new_headers
- for column in self.headers:
-
- if column == self.headers[0]:
- # It's in the headers, so skip it
- continue
-
- # Adding the column name as now they're a regular column
- row_data = [column] + self[column]
- row_data = Row(row_data)
- _dset.append(row=row_data)
-
- return _dset
-
-
- def stack_rows(self, other):
- u"""Stack two :class:`Dataset` instances together by
- joining at the row level, and return new combined
- ``Dataset`` instance."""
-
- if not isinstance(other, Dataset):
- return
-
- if self.width != other.width:
- raise InvalidDimensions
-
- # Copy the source data
- _dset = copy(self)
-
- rows_to_stack = [row for row in _dset._data]
- other_rows = [row for row in other._data]
-
- rows_to_stack.extend(other_rows)
- _dset._data = rows_to_stack
-
- return _dset
-
-
- def stack_columns(self, other):
- u"""Stack two :class:`Dataset` instances together by
- joining at the column level, and return a new
- combined ``Dataset`` instance. If either ``Dataset``
- has headers set, than the other must as well."""
-
- if not isinstance(other, Dataset):
- return
-
- if self.headers or other.headers:
- if not self.headers or not other.headers:
- raise HeadersNeeded
-
- if self.height != other.height:
- raise InvalidDimensions
-
- try:
- new_headers = self.headers + other.headers
- except TypeError:
- new_headers = None
-
- _dset = Dataset()
-
- for column in self.headers:
- _dset.append(col=self[column])
-
- for column in other.headers:
- _dset.append(col=other[column])
-
- _dset.headers = new_headers
-
- return _dset
-
-
- def wipe(self):
- u"""Removes all content and headers from the :class:`Dataset` object."""
- self._data = list()
- self.__headers = None
-
-
-
-class Databook(object):
- u"""A book of :class:`Dataset` objects.
- """
-
- def __init__(self, sets=None):
-
- if sets is None:
- self._datasets = list()
- else:
- self._datasets = sets
-
- self._register_formats()
-
- def __repr__(self):
- try:
- return u'<%s databook>' % (self.title.lower())
- except AttributeError:
- return u'<databook object>'
-
-
- def wipe(self):
- u"""Removes all :class:`Dataset` objects from the :class:`Databook`."""
- self._datasets = []
-
-
- @classmethod
- def _register_formats(cls):
- u"""Adds format properties."""
- for fmt in formats.available:
- try:
- try:
- setattr(cls, fmt.title, property(fmt.export_book, fmt.import_book))
- except AttributeError:
- setattr(cls, fmt.title, property(fmt.export_book))
-
- except AttributeError:
- pass
-
-
- def add_sheet(self, dataset):
- u"""Adds given :class:`Dataset` to the :class:`Databook`."""
- if type(dataset) is Dataset:
- self._datasets.append(dataset)
- else:
- raise InvalidDatasetType
-
-
- def _package(self):
- u"""Packages :class:`Databook` for delivery."""
- collector = []
- for dset in self._datasets:
- collector.append(OrderedDict(
- title = dset.title,
- data = dset.dict
- ))
- return collector
-
-
- @property
- def size(self):
- u"""The number of the :class:`Dataset` objects within :class:`Databook`."""
- return len(self._datasets)
-
-
-def detect(stream):
- u"""Return (format, stream) of given stream."""
- for fmt in formats.available:
- try:
- if fmt.detect(stream):
- return (fmt, stream)
- except AttributeError:
- pass
- return (None, stream)
-
-
-def import_set(stream):
- u"""Return dataset of given stream."""
- (format, stream) = detect(stream)
-
- try:
- data = Dataset()
- format.import_set(data, stream)
- return data
-
- except AttributeError, e:
- return None
-
-
-class InvalidDatasetType(Exception):
- u"Only Datasets can be added to a DataBook"
-
-
-class InvalidDimensions(Exception):
- u"Invalid size"
-
-class InvalidDatasetIndex(Exception):
- u"Outside of Dataset size"
-
-class HeadersNeeded(Exception):
- u"Header parameter must be given when appending a column in this Dataset."
-
-class UnsupportedFormat(NotImplementedError):
- u"Format is not supported"
diff --git a/tablib/formats/__init__.py b/tablib/formats/__init__.py
index 305026d..306ca30 100644
--- a/tablib/formats/__init__.py
+++ b/tablib/formats/__init__.py
@@ -9,5 +9,6 @@ from . import _xls as xls
from . import _yaml as yaml
from . import _tsv as tsv
from . import _html as html
+from . import _xlsx as xlsx
-available = (json, xls, yaml, csv, tsv, html)
+available = (json, xls, yaml, csv, tsv, html, xlsx)
diff --git a/tablib/formats/_xls.py b/tablib/formats/_xls.py
index d820250..48dcc0b 100644
--- a/tablib/formats/_xls.py
+++ b/tablib/formats/_xls.py
@@ -5,15 +5,7 @@
import sys
-
-if sys.version_info[0] > 2:
- from io import BytesIO
- import tablib.packages.xlwt3 as xlwt
-
-else:
- from cStringIO import StringIO as BytesIO
- import tablib.packages.xlwt as xlwt
-
+from tablib.compat import BytesIO, xlwt
title = 'xls'
diff --git a/tablib/formats/_xlsx.py b/tablib/formats/_xlsx.py
new file mode 100644
index 0000000..9cd63b5
--- /dev/null
+++ b/tablib/formats/_xlsx.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+
+""" Tablib - XLSX Support.
+"""
+
+import sys
+
+
+if sys.version_info[0] > 2:
+ from io import BytesIO
+else:
+ from cStringIO import StringIO as BytesIO
+
+from tablib.compat import openpyxl
+
+Workbook = openpyxl.workbook.Workbook
+ExcelWriter = openpyxl.writer.excel.ExcelWriter
+get_column_letter = openpyxl.cell.get_column_letter
+
+from tablib.compat import unicode
+
+
+title = 'xlsx'
+extentions = ('xlsx',)
+
+def export_set(dataset):
+ """Returns XLSX representation of Dataset."""
+
+ wb = Workbook()
+ ws = wb.worksheets[0]
+ ws.title = dataset.title if dataset.title else 'Tablib Dataset'
+
+ dset_sheet(dataset, ws)
+
+ stream = BytesIO()
+ wb.save(stream)
+ return stream.getvalue()
+
+
+def export_book(databook):
+ """Returns XLSX representation of DataBook."""
+
+ wb = Workbook()
+ ew = ExcelWriter(workbook = wb)
+ for i, dset in enumerate(databook._datasets):
+ ws = wb.create_sheet()
+ ws.title = dset.title if dset.title else 'Sheet%s' % (i)
+
+ dset_sheet(dset, ws)
+
+
+ stream = BytesIO()
+ ew.save(stream)
+ return stream.getvalue()
+
+
+def dset_sheet(dataset, ws):
+ """Completes given worksheet from given Dataset."""
+ _package = dataset._package(dicts=False)
+
+ 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
+ for j, col in enumerate(row):
+ col_idx = get_column_letter(j + 1)
+
+ # bold headers
+ if (row_number == 1) and dataset.headers:
+ # ws.cell('%s%s'%(col_idx, row_number)).value = unicode(
+ # '%s' % col, errors='ignore')
+ ws.cell('%s%s'%(col_idx, row_number)).value = unicode(col)
+ style = ws.get_style('%s%s' % (col_idx, row_number))
+ style.font.bold = True
+ ws.freeze_panes = '%s%s' % (col_idx, row_number)
+
+
+ # bold separators
+ elif len(row) < dataset.width:
+ ws.cell('%s%s'%(col_idx, row_number)).value = unicode(
+ '%s' % col, errors='ignore')
+ style = ws.get_style('%s%s' % (col_idx, row_number))
+ style.font.bold = True
+
+ # wrap the rest
+ else:
+ try:
+ if '\n' in col:
+ ws.cell('%s%s'%(col_idx, row_number)).value = unicode(
+ '%s' % col, errors='ignore')
+ style = ws.get_style('%s%s' % (col_idx, row_number))
+ style.alignment.wrap_text
+ else:
+ ws.cell('%s%s'%(col_idx, row_number)).value = unicode(
+ '%s' % col, errors='ignore')
+ except TypeError:
+ ws.cell('%s%s'%(col_idx, row_number)).value = unicode(col)
+
+
diff --git a/tablib/packages/openpyxl/__init__.py b/tablib/packages/openpyxl/__init__.py
new file mode 100644
index 0000000..81381d7
--- /dev/null
+++ b/tablib/packages/openpyxl/__init__.py
@@ -0,0 +1,53 @@
+# file openpyxl/__init__.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Imports for the openpyxl package."""
+
+# package imports
+from . import cell
+from . import namedrange
+from . import style
+from . import workbook
+from . import worksheet
+from . import reader
+from . import shared
+from . import writer
+
+# constants
+
+__major__ = 1 # for major interface/format changes
+__minor__ = 5 # for minor interface/format changes
+__release__ = 2 # for tweaks, bug-fixes, or development
+
+__version__ = '%d.%d.%d' % (__major__, __minor__, __release__)
+
+__author__ = 'Eric Gazoni'
+__license__ = 'MIT/Expat'
+__author_email__ = 'eric.gazoni@gmail.com'
+__maintainer_email__ = 'openpyxl-users@googlegroups.com'
+__url__ = 'http://bitbucket.org/ericgazoni/openpyxl/wiki/Home'
+__downloadUrl__ = "http://bitbucket.org/ericgazoni/openpyxl/downloads"
+
+__all__ = ('reader', 'shared', 'writer',)
diff --git a/tablib/packages/openpyxl/cell.py b/tablib/packages/openpyxl/cell.py
new file mode 100644
index 0000000..757a834
--- /dev/null
+++ b/tablib/packages/openpyxl/cell.py
@@ -0,0 +1,384 @@
+# file openpyxl/cell.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Manage individual cells in a spreadsheet.
+
+The Cell class is required to know its value and type, display options,
+and any other features of an Excel cell. Utilities for referencing
+cells using Excel's 'A1' column/row nomenclature are also provided.
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+# Python stdlib imports
+import datetime
+import re
+
+# package imports
+from .shared.date_time import SharedDate
+from .shared.exc import CellCoordinatesException, \
+ ColumnStringIndexException, DataTypeException
+from .style import NumberFormat
+
+# constants
+COORD_RE = re.compile('^[$]?([A-Z]+)[$]?(\d+)$')
+
+ABSOLUTE_RE = re.compile('^[$]?([A-Z]+)[$]?(\d+)(:[$]?([A-Z]+)[$]?(\d+))?$')
+
+def coordinate_from_string(coord_string):
+ """Convert a coordinate string like 'B12' to a tuple ('B', 12)"""
+ match = COORD_RE.match(coord_string.upper())
+ if not match:
+ msg = 'Invalid cell coordinates (%s)' % coord_string
+ raise CellCoordinatesException(msg)
+ column, row = match.groups()
+ return (column, int(row))
+
+
+def absolute_coordinate(coord_string):
+ """Convert a coordinate to an absolute coordinate string (B12 -> $B$12)"""
+ parts = ABSOLUTE_RE.match(coord_string).groups()
+
+ if all(parts[-2:]):
+ return '$%s$%s:$%s$%s' % (parts[0], parts[1], parts[3], parts[4])
+ else:
+ return '$%s$%s' % (parts[0], parts[1])
+
+
+def column_index_from_string(column, fast = False):
+ """Convert a column letter into a column number (e.g. B -> 2)
+
+ Excel only supports 1-3 letter column names from A -> ZZZ, so we
+ restrict our column names to 1-3 characters, each in the range A-Z.
+
+ .. note::
+
+ Fast mode is faster but does not check that all letters are capitals between A and Z
+
+ """
+ column = column.upper()
+
+ clen = len(column)
+
+ if not fast and not all('A' <= char <= 'Z' for char in column):
+ msg = 'Column string must contain only characters A-Z: got %s' % column
+ raise ColumnStringIndexException(msg)
+
+ if clen == 1:
+ return ord(column[0]) - 64
+ elif clen == 2:
+ return ((1 + (ord(column[0]) - 65)) * 26) + (ord(column[1]) - 64)
+ elif clen == 3:
+ return ((1 + (ord(column[0]) - 65)) * 676) + ((1 + (ord(column[1]) - 65)) * 26) + (ord(column[2]) - 64)
+ elif clen > 3:
+ raise ColumnStringIndexException('Column string index can not be longer than 3 characters')
+ else:
+ raise ColumnStringIndexException('Column string index can not be empty')
+
+
+def get_column_letter(col_idx):
+ """Convert a column number into a column letter (3 -> 'C')
+
+ Right shift the column col_idx by 26 to find column letters in reverse
+ order. These numbers are 1-based, and can be converted to ASCII
+ ordinals by adding 64.
+
+ """
+ # these indicies corrospond to A -> ZZZ and include all allowed
+ # columns
+ if not 1 <= col_idx <= 18278:
+ msg = 'Column index out of bounds: %s' % col_idx
+ raise ColumnStringIndexException(msg)
+ ordinals = []
+ temp = col_idx
+ while temp:
+ quotient, remainder = divmod(temp, 26)
+ # check for exact division and borrow if needed
+ if remainder == 0:
+ quotient -= 1
+ remainder = 26
+ ordinals.append(remainder + 64)
+ temp = quotient
+ ordinals.reverse()
+ return ''.join([chr(ordinal) for ordinal in ordinals])
+
+
+class Cell(object):
+ """Describes cell associated properties.
+
+ Properties of interest include style, type, value, and address.
+
+ """
+ __slots__ = ('column',
+ 'row',
+ '_value',
+ '_data_type',
+ 'parent',
+ 'xf_index',
+ '_hyperlink_rel')
+
+ ERROR_CODES = {'#NULL!': 0,
+ '#DIV/0!': 1,
+ '#VALUE!': 2,
+ '#REF!': 3,
+ '#NAME?': 4,
+ '#NUM!': 5,
+ '#N/A': 6}
+
+ TYPE_STRING = 's'
+ TYPE_FORMULA = 'f'
+ TYPE_NUMERIC = 'n'
+ TYPE_BOOL = 'b'
+ TYPE_NULL = 's'
+ TYPE_INLINE = 'inlineStr'
+ TYPE_ERROR = 'e'
+
+ VALID_TYPES = [TYPE_STRING, TYPE_FORMULA, TYPE_NUMERIC, TYPE_BOOL,
+ TYPE_NULL, TYPE_INLINE, TYPE_ERROR]
+
+ RE_PATTERNS = {
+ 'percentage': re.compile('^\-?[0-9]*\.?[0-9]*\s?\%$'),
+ 'time': re.compile('^(\d|[0-1]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$'),
+ 'numeric': re.compile('^\-?([0-9]+\\.?[0-9]*|[0-9]*\\.?[0-9]+)((E|e)\-?[0-9]+)?$'), }
+
+ def __init__(self, worksheet, column, row, value = None):
+ self.column = column.upper()
+ self.row = row
+ # _value is the stored value, while value is the displayed value
+ self._value = None
+ self._hyperlink_rel = None
+ self._data_type = self.TYPE_NULL
+ if value:
+ self.value = value
+ self.parent = worksheet
+ self.xf_index = 0
+
+ def __repr__(self):
+ return "<Cell %s.%s>" % (self.parent.title, self.get_coordinate())
+
+ def check_string(self, value):
+ """Check string coding, length, and line break character"""
+ # convert to unicode string
+ value = unicode(value)
+ # string must never be longer than 32,767 characters
+ # truncate if necessary
+ value = value[:32767]
+ # we require that newline is represented as "\n" in core,
+ # not as "\r\n" or "\r"
+ value = value.replace('\r\n', '\n')
+ return value
+
+ def check_numeric(self, value):
+ """Cast value to int or float if necessary"""
+ if not isinstance(value, (int, float)):
+ try:
+ value = int(value)
+ except ValueError:
+ value = float(value)
+ return value
+
+ def set_value_explicit(self, value = None, data_type = TYPE_STRING):
+ """Coerce values according to their explicit type"""
+ type_coercion_map = {
+ self.TYPE_INLINE: self.check_string,
+ self.TYPE_STRING: self.check_string,
+ self.TYPE_FORMULA: unicode,
+ self.TYPE_NUMERIC: self.check_numeric,
+ self.TYPE_BOOL: bool, }
+ try:
+ self._value = type_coercion_map[data_type](value)
+ except KeyError:
+ if data_type not in self.VALID_TYPES:
+ msg = 'Invalid data type: %s' % data_type
+ raise DataTypeException(msg)
+ self._data_type = data_type
+
+ def data_type_for_value(self, value):
+ """Given a value, infer the correct data type"""
+ if value is None:
+ data_type = self.TYPE_NULL
+ elif value is True or value is False:
+ data_type = self.TYPE_BOOL
+ elif isinstance(value, (int, float)):
+ data_type = self.TYPE_NUMERIC
+ elif not value:
+ data_type = self.TYPE_STRING
+ elif isinstance(value, (datetime.datetime, datetime.date)):
+ data_type = self.TYPE_NUMERIC
+ elif isinstance(value, basestring) and value[0] == '=':
+ data_type = self.TYPE_FORMULA
+ elif self.RE_PATTERNS['numeric'].match(value):
+ data_type = self.TYPE_NUMERIC
+ elif value.strip() in self.ERROR_CODES:
+ data_type = self.TYPE_ERROR
+ else:
+ data_type = self.TYPE_STRING
+ return data_type
+
+ def bind_value(self, value):
+ """Given a value, infer type and display options."""
+ self._data_type = self.data_type_for_value(value)
+ if value is None:
+ self.set_value_explicit('', self.TYPE_NULL)
+ return True
+ elif self._data_type == self.TYPE_STRING:
+ # percentage detection
+ percentage_search = self.RE_PATTERNS['percentage'].match(value)
+ if percentage_search and value.strip() != '%':
+ value = float(value.replace('%', '')) / 100.0
+ self.set_value_explicit(value, self.TYPE_NUMERIC)
+ self._set_number_format(NumberFormat.FORMAT_PERCENTAGE)
+ return True
+ # time detection
+ time_search = self.RE_PATTERNS['time'].match(value)
+ if time_search:
+ sep_count = value.count(':') #pylint: disable-msg=E1103
+ if sep_count == 1:
+ hours, minutes = [int(bit) for bit in value.split(':')] #pylint: disable-msg=E1103
+ seconds = 0
+ elif sep_count == 2:
+ hours, minutes, seconds = \
+ [int(bit) for bit in value.split(':')] #pylint: disable-msg=E1103
+ days = (hours / 24.0) + (minutes / 1440.0) + \
+ (seconds / 86400.0)
+ self.set_value_explicit(days, self.TYPE_NUMERIC)
+ self._set_number_format(NumberFormat.FORMAT_DATE_TIME3)
+ return True
+ if self._data_type == self.TYPE_NUMERIC:
+ # date detection
+ # if the value is a date, but not a date time, make it a
+ # datetime, and set the time part to 0
+ if isinstance(value, datetime.date) and not \
+ isinstance(value, datetime.datetime):
+ value = datetime.datetime.combine(value, datetime.time())
+ if isinstance(value, datetime.datetime):
+ value = SharedDate().datetime_to_julian(date = value)
+ self.set_value_explicit(value, self.TYPE_NUMERIC)
+ self._set_number_format(NumberFormat.FORMAT_DATE_YYYYMMDD2)
+ return True
+ self.set_value_explicit(value, self._data_type)
+
+ def _get_value(self):
+ """Return the value, formatted as a date if needed"""
+ value = self._value
+ if self.is_date():
+ value = SharedDate().from_julian(value)
+ return value
+
+ def _set_value(self, value):
+ """Set the value and infer type and display options."""
+ self.bind_value(value)
+
+ value = property(_get_value, _set_value,
+ doc = 'Get or set the value held in the cell.\n\n'
+ ':rtype: depends on the value (string, float, int or '
+ ':class:`datetime.datetime`)')
+
+ def _set_hyperlink(self, val):
+ """Set value and display for hyperlinks in a cell"""
+ if self._hyperlink_rel is None:
+ self._hyperlink_rel = self.parent.create_relationship("hyperlink")
+ self._hyperlink_rel.target = val
+ self._hyperlink_rel.target_mode = "External"
+ if self._value is None:
+ self.value = val
+
+ def _get_hyperlink(self):
+ """Return the hyperlink target or an empty string"""
+ return self._hyperlink_rel is not None and \
+ self._hyperlink_rel.target or ''
+
+ hyperlink = property(_get_hyperlink, _set_hyperlink,
+ doc = 'Get or set the hyperlink held in the cell. '
+ 'Automatically sets the `value` of the cell with link text, '
+ 'but you can modify it afterwards by setting the '
+ '`value` property, and the hyperlink will remain.\n\n'
+ ':rtype: string')
+
+ @property
+ def hyperlink_rel_id(self):
+ """Return the id pointed to by the hyperlink, or None"""
+ return self._hyperlink_rel is not None and \
+ self._hyperlink_rel.id or None
+
+ def _set_number_format(self, format_code):
+ """Set a new formatting code for numeric values"""
+ self.style.number_format.format_code = format_code
+
+ @property
+ def has_style(self):
+ """Check if the parent worksheet has a style for this cell"""
+ return self.get_coordinate() in self.parent._styles #pylint: disable-msg=W0212
+
+ @property
+ def style(self):
+ """Returns the :class:`openpyxl.style.Style` object for this cell"""
+ return self.parent.get_style(self.get_coordinate())
+
+ @property
+ def data_type(self):
+ """Return the data type represented by this cell"""
+ return self._data_type
+
+ def get_coordinate(self):
+ """Return the coordinate string for this cell (e.g. 'B12')
+
+ :rtype: string
+ """
+ return '%s%s' % (self.column, self.row)
+
+ @property
+ def address(self):
+ """Return the coordinate string for this cell (e.g. 'B12')
+
+ :rtype: string
+ """
+ return self.get_coordinate()
+
+ def offset(self, row = 0, column = 0):
+ """Returns a cell location relative to this cell.
+
+ :param row: number of rows to offset
+ :type row: int
+
+ :param column: number of columns to offset
+ :type column: int
+
+ :rtype: :class:`openpyxl.cell.Cell`
+ """
+ offset_column = get_column_letter(column_index_from_string(
+ column = self.column) + column)
+ offset_row = self.row + row
+ return self.parent.cell('%s%s' % (offset_column, offset_row))
+
+ def is_date(self):
+ """Returns whether the value is *probably* a date or not
+
+ :rtype: bool
+ """
+ return (self.has_style
+ and self.style.number_format.is_date_format()
+ and isinstance(self._value, (int, float)))
diff --git a/tablib/packages/openpyxl/chart.py b/tablib/packages/openpyxl/chart.py
new file mode 100644
index 0000000..56cbb4d
--- /dev/null
+++ b/tablib/packages/openpyxl/chart.py
@@ -0,0 +1,340 @@
+'''
+Copyright (c) 2010 openpyxl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+@license: http://www.opensource.org/licenses/mit-license.php
+@author: Eric Gazoni
+'''
+
+import math
+
+from .style import NumberFormat
+from .drawing import Drawing, Shape
+from .shared.units import pixels_to_EMU, short_color
+from .cell import get_column_letter
+
+class Axis(object):
+
+ POSITION_BOTTOM = 'b'
+ POSITION_LEFT = 'l'
+
+ ORIENTATION_MIN_MAX = "minMax"
+
+ def __init__(self):
+
+ self.orientation = self.ORIENTATION_MIN_MAX
+ self.number_format = NumberFormat()
+ for attr in ('position','tick_label_position','crosses',
+ 'auto','label_align','label_offset','cross_between'):
+ setattr(self, attr, None)
+ self.min = 0
+ self.max = None
+ self.unit = None
+
+ @classmethod
+ def default_category(cls):
+ """ default values for category axes """
+
+ ax = Axis()
+ ax.id = 60871424
+ ax.cross = 60873344
+ ax.position = Axis.POSITION_BOTTOM
+ ax.tick_label_position = 'nextTo'
+ ax.crosses = "autoZero"
+ ax.auto = True
+ ax.label_align = 'ctr'
+ ax.label_offset = 100
+ return ax
+
+ @classmethod
+ def default_value(cls):
+ """ default values for value axes """
+
+ ax = Axis()
+ ax.id = 60873344
+ ax.cross = 60871424
+ ax.position = Axis.POSITION_LEFT
+ ax.major_gridlines = None
+ ax.tick_label_position = 'nextTo'
+ ax.crosses = 'autoZero'
+ ax.auto = False
+ ax.cross_between = 'between'
+ return ax
+
+class Reference(object):
+ """ a simple wrapper around a serie of reference data """
+
+ def __init__(self, sheet, pos1, pos2=None):
+
+ self.sheet = sheet
+ self.pos1 = pos1
+ self.pos2 = pos2
+
+ def get_type(self):
+
+ if isinstance(self.cache[0], basestring):
+ return 'str'
+ else:
+ return 'num'
+
+ def _get_ref(self):
+ """ format excel reference notation """
+
+ if self.pos2:
+ return '%s!$%s$%s:$%s$%s' % (self.sheet.title,
+ get_column_letter(self.pos1[1]+1), self.pos1[0]+1,
+ get_column_letter(self.pos2[1]+1), self.pos2[0]+1)
+ else:
+ return '%s!$%s$%s' % (self.sheet.title,
+ get_column_letter(self.pos1[1]+1), self.pos1[0]+1)
+
+
+ def _get_cache(self):
+ """ read data in sheet - to be used at writing time """
+
+ cache = []
+ if self.pos2:
+ for row in range(self.pos1[0], self.pos2[0]+1):
+ for col in range(self.pos1[1], self.pos2[1]+1):
+ cache.append(self.sheet.cell(row=row, column=col).value)
+ else:
+ cell = self.sheet.cell(row=self.pos1[0], column=self.pos1[1])
+ cache.append(cell.value)
+ return cache
+
+
+class Serie(object):
+ """ a serie of data and possibly associated labels """
+
+ MARKER_NONE = 'none'
+
+ def __init__(self, values, labels=None, legend=None, color=None, xvalues=None):
+
+ self.marker = Serie.MARKER_NONE
+ self.values = values
+ self.xvalues = xvalues
+ self.labels = labels
+ self.legend = legend
+ self.error_bar = None
+ self._color = color
+
+ def _get_color(self):
+ return self._color
+
+ def _set_color(self, color):
+ self._color = short_color(color)
+
+ color = property(_get_color, _set_color)
+
+ def get_min_max(self):
+
+ if self.error_bar:
+ err_cache = self.error_bar.values._get_cache()
+ vals = [v + err_cache[i] \
+ for i,v in enumerate(self.values._get_cache())]
+ else:
+ vals = self.values._get_cache()
+ return min(vals), max(vals)
+
+ def __len__(self):
+
+ return len(self.values.cache)
+
+class Legend(object):
+
+ def __init__(self):
+
+ self.position = 'r'
+ self.layout = None
+
+class ErrorBar(object):
+
+ PLUS = 1
+ MINUS = 2
+ PLUS_MINUS = 3
+
+ def __init__(self, _type, values):
+
+ self.type = _type
+ self.values = values
+
+class Chart(object):
+ """ raw chart class """
+
+ GROUPING_CLUSTERED = 'clustered'
+ GROUPING_STANDARD = 'standard'
+
+ BAR_CHART = 1
+ LINE_CHART = 2
+ SCATTER_CHART = 3
+
+ def __init__(self, _type, grouping):
+
+ self._series = []
+
+ # public api
+ self.type = _type
+ self.grouping = grouping
+ self.x_axis = Axis.default_category()
+ self.y_axis = Axis.default_value()
+ self.legend = Legend()
+ self.lang = 'fr-FR'
+ self.title = ''
+ self.print_margins = dict(b=.75, l=.7, r=.7, t=.75, header=0.3, footer=.3)
+
+ # the containing drawing
+ self.drawing = Drawing()
+
+ # the offset for the plot part in percentage of the drawing size
+ self.width = .6
+ self.height = .6
+ self.margin_top = self._get_max_margin_top()
+ self.margin_left = 0
+
+ # the user defined shapes
+ self._shapes = []
+
+ def add_serie(self, serie):
+
+ serie.id = len(self._series)
+ self._series.append(serie)
+ self._compute_min_max()
+ if not None in [s.xvalues for s in self._series]:
+ self._compute_xmin_xmax()
+
+ def add_shape(self, shape):
+
+ shape._chart = self
+ self._shapes.append(shape)
+
+ def get_x_units(self):
+ """ calculate one unit for x axis in EMU """
+
+ return max([len(s.values._get_cache()) for s in self._series])
+
+ def get_y_units(self):
+ """ calculate one unit for y axis in EMU """
+
+ dh = pixels_to_EMU(self.drawing.height)
+ return (dh * self.height) / self.y_axis.max
+
+ def get_y_chars(self):
+ """ estimate nb of chars for y axis """
+
+ _max = max([max(s.values._get_cache()) for s in self._series])
+ return len(str(int(_max)))
+
+ def _compute_min_max(self):
+ """ compute y axis limits and units """
+
+ maxi = max([max(s.values._get_cache()) for s in self._series])
+
+ mul = None
+ if maxi < 1:
+ s = str(maxi).split('.')[1]
+ mul = 10
+ for x in s:
+ if x == '0':
+ mul *= 10
+ else:
+ break
+ maxi = maxi * mul
+
+ maxi = math.ceil(maxi * 1.1)
+ sz = len(str(int(maxi))) - 1
+ unit = math.ceil(math.ceil(maxi / pow(10, sz)) * pow(10, sz-1))
+ maxi = math.ceil(maxi/unit) * unit
+
+ if mul is not None:
+ maxi = maxi/mul
+ unit = unit/mul
+
+ if maxi / unit > 9:
+ # no more that 10 ticks
+ unit *= 2
+
+ self.y_axis.max = maxi
+ self.y_axis.unit = unit
+
+ def _compute_xmin_xmax(self):
+ """ compute x axis limits and units """
+
+ maxi = max([max(s.xvalues._get_cache()) for s in self._series])
+
+ mul = None
+ if maxi < 1:
+ s = str(maxi).split('.')[1]
+ mul = 10
+ for x in s:
+ if x == '0':
+ mul *= 10
+ else:
+ break
+ maxi = maxi * mul
+
+ maxi = math.ceil(maxi * 1.1)
+ sz = len(str(int(maxi))) - 1
+ unit = math.ceil(math.ceil(maxi / pow(10, sz)) * pow(10, sz-1))
+ maxi = math.ceil(maxi/unit) * unit
+
+ if mul is not None:
+ maxi = maxi/mul
+ unit = unit/mul
+
+ if maxi / unit > 9:
+ # no more that 10 ticks
+ unit *= 2
+
+ self.x_axis.max = maxi
+ self.x_axis.unit = unit
+
+ def _get_max_margin_top(self):
+
+ mb = Shape.FONT_HEIGHT + Shape.MARGIN_BOTTOM
+ plot_height = self.drawing.height * self.height
+ return float(self.drawing.height - plot_height - mb)/self.drawing.height
+
+ def _get_min_margin_left(self):
+
+ ml = (self.get_y_chars() * Shape.FONT_WIDTH) + Shape.MARGIN_LEFT
+ return float(ml)/self.drawing.width
+
+ def _get_margin_top(self):
+ """ get margin in percent """
+
+ return min(self.margin_top, self._get_max_margin_top())
+
+ def _get_margin_left(self):
+
+ return max(self._get_min_margin_left(), self.margin_left)
+
+class BarChart(Chart):
+ def __init__(self):
+ super(BarChart, self).__init__(Chart.BAR_CHART, Chart.GROUPING_CLUSTERED)
+
+class LineChart(Chart):
+ def __init__(self):
+ super(LineChart, self).__init__(Chart.LINE_CHART, Chart.GROUPING_STANDARD)
+
+class ScatterChart(Chart):
+ def __init__(self):
+ super(ScatterChart, self).__init__(Chart.SCATTER_CHART, Chart.GROUPING_STANDARD)
+
+
diff --git a/tablib/packages/openpyxl/drawing.py b/tablib/packages/openpyxl/drawing.py
new file mode 100644
index 0000000..610324e
--- /dev/null
+++ b/tablib/packages/openpyxl/drawing.py
@@ -0,0 +1,401 @@
+'''
+Copyright (c) 2010 openpyxl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+@license: http://www.opensource.org/licenses/mit-license.php
+@author: Eric Gazoni
+'''
+
+import math
+from .style import Color
+from .shared.units import pixels_to_EMU, EMU_to_pixels, short_color
+
+class Shadow(object):
+
+ SHADOW_BOTTOM = 'b'
+ SHADOW_BOTTOM_LEFT = 'bl'
+ SHADOW_BOTTOM_RIGHT = 'br'
+ SHADOW_CENTER = 'ctr'
+ SHADOW_LEFT = 'l'
+ SHADOW_TOP = 't'
+ SHADOW_TOP_LEFT = 'tl'
+ SHADOW_TOP_RIGHT = 'tr'
+
+ def __init__(self):
+ self.visible = False
+ self.blurRadius = 6
+ self.distance = 2
+ self.direction = 0
+ self.alignment = self.SHADOW_BOTTOM_RIGHT
+ self.color = Color(Color.BLACK)
+ self.alpha = 50
+
+class Drawing(object):
+ """ a drawing object - eg container for shapes or charts
+ we assume user specifies dimensions in pixels; units are
+ converted to EMU in the drawing part
+ """
+
+ count = 0
+
+ def __init__(self):
+
+ self.name = ''
+ self.description = ''
+ self.coordinates = ((1,2), (16,8))
+ self.left = 0
+ self.top = 0
+ self._width = EMU_to_pixels(200000)
+ self._height = EMU_to_pixels(1828800)
+ self.resize_proportional = False
+ self.rotation = 0
+# self.shadow = Shadow()
+
+ def _set_width(self, w):
+
+ if self.resize_proportional and w:
+ ratio = self._height / self._width
+ self._height = round(ratio * w)
+ self._width = w
+
+ def _get_width(self):
+
+ return self._width
+
+ width = property(_get_width, _set_width)
+
+ def _set_height(self, h):
+
+ if self.resize_proportional and h:
+ ratio = self._width / self._height
+ self._width = round(ratio * h)
+ self._height = h
+
+ def _get_height(self):
+
+ return self._height
+
+ height = property(_get_height, _set_height)
+
+ def set_dimension(self, w=0, h=0):
+
+ xratio = w / self._width
+ yratio = h / self._height
+
+ if self.resize_proportional and w and h:
+ if (xratio * self._height) < h:
+ self._height = math.ceil(xratio * self._height)
+ self._width = width
+ else:
+ self._width = math.ceil(yratio * self._width)
+ self._height = height
+
+ def get_emu_dimensions(self):
+ """ return (x, y, w, h) in EMU """
+
+ return (pixels_to_EMU(self.left), pixels_to_EMU(self.top),
+ pixels_to_EMU(self._width), pixels_to_EMU(self._height))
+
+
+class Shape(object):
+ """ a drawing inside a chart
+ coordiantes are specified by the user in the axis units
+ """
+
+ MARGIN_LEFT = 6 + 13 + 1
+ MARGIN_BOTTOM = 17 + 11
+
+ FONT_WIDTH = 7
+ FONT_HEIGHT = 8
+
+ ROUND_RECT = 'roundRect'
+ RECT = 'rect'
+
+ # other shapes to define :
+ '''
+ "line"
+ "lineInv"
+ "triangle"
+ "rtTriangle"
+ "diamond"
+ "parallelogram"
+ "trapezoid"
+ "nonIsoscelesTrapezoid"
+ "pentagon"
+ "hexagon"
+ "heptagon"
+ "octagon"
+ "decagon"
+ "dodecagon"
+ "star4"
+ "star5"
+ "star6"
+ "star7"
+ "star8"
+ "star10"
+ "star12"
+ "star16"
+ "star24"
+ "star32"
+ "roundRect"
+ "round1Rect"
+ "round2SameRect"
+ "round2DiagRect"
+ "snipRoundRect"
+ "snip1Rect"
+ "snip2SameRect"
+ "snip2DiagRect"
+ "plaque"
+ "ellipse"
+ "teardrop"
+ "homePlate"
+ "chevron"
+ "pieWedge"
+ "pie"
+ "blockArc"
+ "donut"
+ "noSmoking"
+ "rightArrow"
+ "leftArrow"
+ "upArrow"
+ "downArrow"
+ "stripedRightArrow"
+ "notchedRightArrow"
+ "bentUpArrow"
+ "leftRightArrow"
+ "upDownArrow"
+ "leftUpArrow"
+ "leftRightUpArrow"
+ "quadArrow"
+ "leftArrowCallout"
+ "rightArrowCallout"
+ "upArrowCallout"
+ "downArrowCallout"
+ "leftRightArrowCallout"
+ "upDownArrowCallout"
+ "quadArrowCallout"
+ "bentArrow"
+ "uturnArrow"
+ "circularArrow"
+ "leftCircularArrow"
+ "leftRightCircularArrow"
+ "curvedRightArrow"
+ "curvedLeftArrow"
+ "curvedUpArrow"
+ "curvedDownArrow"
+ "swooshArrow"
+ "cube"
+ "can"
+ "lightningBolt"
+ "heart"
+ "sun"
+ "moon"
+ "smileyFace"
+ "irregularSeal1"
+ "irregularSeal2"
+ "foldedCorner"
+ "bevel"
+ "frame"
+ "halfFrame"
+ "corner"
+ "diagStripe"
+ "chord"
+ "arc"
+ "leftBracket"
+ "rightBracket"
+ "leftBrace"
+ "rightBrace"
+ "bracketPair"
+ "bracePair"
+ "straightConnector1"
+ "bentConnector2"
+ "bentConnector3"
+ "bentConnector4"
+ "bentConnector5"
+ "curvedConnector2"
+ "curvedConnector3"
+ "curvedConnector4"
+ "curvedConnector5"
+ "callout1"
+ "callout2"
+ "callout3"
+ "accentCallout1"
+ "accentCallout2"
+ "accentCallout3"
+ "borderCallout1"
+ "borderCallout2"
+ "borderCallout3"
+ "accentBorderCallout1"
+ "accentBorderCallout2"
+ "accentBorderCallout3"
+ "wedgeRectCallout"
+ "wedgeRoundRectCallout"
+ "wedgeEllipseCallout"
+ "cloudCallout"
+ "cloud"
+ "ribbon"
+ "ribbon2"
+ "ellipseRibbon"
+ "ellipseRibbon2"
+ "leftRightRibbon"
+ "verticalScroll"
+ "horizontalScroll"
+ "wave"
+ "doubleWave"
+ "plus"
+ "flowChartProcess"
+ "flowChartDecision"
+ "flowChartInputOutput"
+ "flowChartPredefinedProcess"
+ "flowChartInternalStorage"
+ "flowChartDocument"
+ "flowChartMultidocument"
+ "flowChartTerminator"
+ "flowChartPreparation"
+ "flowChartManualInput"
+ "flowChartManualOperation"
+ "flowChartConnector"
+ "flowChartPunchedCard"
+ "flowChartPunchedTape"
+ "flowChartSummingJunction"
+ "flowChartOr"
+ "flowChartCollate"
+ "flowChartSort"
+ "flowChartExtract"
+ "flowChartMerge"
+ "flowChartOfflineStorage"
+ "flowChartOnlineStorage"
+ "flowChartMagneticTape"
+ "flowChartMagneticDisk"
+ "flowChartMagneticDrum"
+ "flowChartDisplay"
+ "flowChartDelay"
+ "flowChartAlternateProcess"
+ "flowChartOffpageConnector"
+ "actionButtonBlank"
+ "actionButtonHome"
+ "actionButtonHelp"
+ "actionButtonInformation"
+ "actionButtonForwardNext"
+ "actionButtonBackPrevious"
+ "actionButtonEnd"
+ "actionButtonBeginning"
+ "actionButtonReturn"
+ "actionButtonDocument"
+ "actionButtonSound"
+ "actionButtonMovie"
+ "gear6"
+ "gear9"
+ "funnel"
+ "mathPlus"
+ "mathMinus"
+ "mathMultiply"
+ "mathDivide"
+ "mathEqual"
+ "mathNotEqual"
+ "cornerTabs"
+ "squareTabs"
+ "plaqueTabs"
+ "chartX"
+ "chartStar"
+ "chartPlus"
+ '''
+
+ def __init__(self, coordinates=((0,0), (1,1)), text=None, scheme="accent1"):
+
+ self.coordinates = coordinates # in axis unit
+ self.text = text
+ self.scheme = scheme
+ self.style = Shape.RECT
+ self._border_width = 3175 # in EMU
+ self._border_color = Color.BLACK[2:] #"F3B3C5"
+ self._color = Color.WHITE[2:]
+ self._text_color = Color.BLACK[2:]
+
+ def _get_border_color(self):
+ return self._border_color
+
+ def _set_border_color(self, color):
+ self._border_color = short_color(color)
+
+ border_color = property(_get_border_color, _set_border_color)
+
+ def _get_color(self):
+ return self._color
+
+ def _set_color(self, color):
+ self._color = short_color(color)
+
+ color = property(_get_color, _set_color)
+
+ def _get_text_color(self):
+ return self._text_color
+
+ def _set_text_color(self, color):
+ self._text_color = short_color(color)
+
+ text_color = property(_get_text_color, _set_text_color)
+
+ def _get_border_width(self):
+
+ return EMU_to_pixels(self._border_width)
+
+ def _set_border_width(self, w):
+
+ self._border_width = pixels_to_EMU(w)
+ # print self._border_width
+
+ border_width = property(_get_border_width, _set_border_width)
+
+ def get_coordinates(self):
+ """ return shape coordinates in percentages (left, top, right, bottom) """
+
+ (x1, y1), (x2, y2) = self.coordinates
+
+ drawing_width = pixels_to_EMU(self._chart.drawing.width)
+ drawing_height = pixels_to_EMU(self._chart.drawing.height)
+ plot_width = drawing_width * self._chart.width
+ plot_height = drawing_height * self._chart.height
+
+ margin_left = self._chart._get_margin_left() * drawing_width
+ xunit = plot_width / self._chart.get_x_units()
+
+ margin_top = self._chart._get_margin_top() * drawing_height
+ yunit = self._chart.get_y_units()
+
+ x_start = (margin_left + (float(x1) * xunit)) / drawing_width
+ y_start = (margin_top + plot_height - (float(y1) * yunit)) / drawing_height
+
+ x_end = (margin_left + (float(x2) * xunit)) / drawing_width
+ y_end = (margin_top + plot_height - (float(y2) * yunit)) / drawing_height
+
+ def _norm_pct(pct):
+ """ force shapes to appear by truncating too large sizes """
+ if pct>1: pct = 1
+ elif pct<0: pct = 0
+ return pct
+
+ # allow user to specify y's in whatever order
+ # excel expect y_end to be lower
+ if y_end < y_start:
+ y_end, y_start = y_start, y_end
+
+ return (_norm_pct(x_start), _norm_pct(y_start),
+ _norm_pct(x_end), _norm_pct(y_end))
diff --git a/tablib/packages/openpyxl/namedrange.py b/tablib/packages/openpyxl/namedrange.py
new file mode 100644
index 0000000..85b08a8
--- /dev/null
+++ b/tablib/packages/openpyxl/namedrange.py
@@ -0,0 +1,68 @@
+# file openpyxl/namedrange.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Track named groups of cells in a worksheet"""
+
+# Python stdlib imports
+import re
+
+# package imports
+from .shared.exc import NamedRangeException
+
+# constants
+NAMED_RANGE_RE = re.compile("'?([^']*)'?!((\$([A-Za-z]+))?\$([0-9]+)(:(\$([A-Za-z]+))?(\$([0-9]+)))?)$")
+
+class NamedRange(object):
+ """A named group of cells"""
+ __slots__ = ('name', 'destinations', 'local_only')
+
+ def __init__(self, name, destinations):
+ self.name = name
+ self.destinations = destinations
+ self.local_only = False
+
+ def __str__(self):
+ return ','.join(['%s!%s' % (sheet, name) for sheet, name in self.destinations])
+
+ def __repr__(self):
+
+ return '<%s "%s">' % (self.__class__.__name__, str(self))
+
+
+def split_named_range(range_string):
+ """Separate a named range into its component parts"""
+
+ destinations = []
+
+ for range_string in range_string.split(','):
+
+ match = NAMED_RANGE_RE.match(range_string)
+ if not match:
+ raise NamedRangeException('Invalid named range string: "%s"' % range_string)
+ else:
+ sheet_name, xlrange = match.groups()[:2]
+ destinations.append((sheet_name, xlrange))
+
+ return destinations
diff --git a/tablib/packages/openpyxl/reader/__init__.py b/tablib/packages/openpyxl/reader/__init__.py
new file mode 100644
index 0000000..9b0ee2f
--- /dev/null
+++ b/tablib/packages/openpyxl/reader/__init__.py
@@ -0,0 +1,33 @@
+# file openpyxl/reader/__init__.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Imports for the openpyxl.reader namespace."""
+
+# package imports
+from ..reader import excel
+from ..reader import strings
+from ..reader import style
+from ..reader import workbook
+from ..reader import worksheet
diff --git a/tablib/packages/openpyxl/reader/excel.py b/tablib/packages/openpyxl/reader/excel.py
new file mode 100644
index 0000000..16c3f91
--- /dev/null
+++ b/tablib/packages/openpyxl/reader/excel.py
@@ -0,0 +1,109 @@
+# file openpyxl/reader/excel.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Read an xlsx file into Python"""
+
+# Python stdlib imports
+from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
+
+# package imports
+from ..shared.exc import OpenModeError, InvalidFileException
+from ..shared.ooxml import ARC_SHARED_STRINGS, ARC_CORE, ARC_APP, \
+ ARC_WORKBOOK, PACKAGE_WORKSHEETS, ARC_STYLE
+from ..workbook import Workbook
+from ..reader.strings import read_string_table
+from ..reader.style import read_style_table
+from ..reader.workbook import read_sheets_titles, read_named_ranges, \
+ read_properties_core, get_sheet_ids
+from ..reader.worksheet import read_worksheet
+from ..reader.iter_worksheet import unpack_worksheet
+
+def load_workbook(filename, use_iterators = False):
+ """Open the given filename and return the workbook
+
+ :param filename: the path to open
+ :type filename: string
+
+ :param use_iterators: use lazy load for cells
+ :type use_iterators: bool
+
+ :rtype: :class:`openpyxl.workbook.Workbook`
+
+ .. note::
+
+ When using lazy load, all worksheets will be :class:`openpyxl.reader.iter_worksheet.IterableWorksheet`
+ and the returned workbook will be read-only.
+
+ """
+
+ if isinstance(filename, file):
+ # fileobject must have been opened with 'rb' flag
+ # it is required by zipfile
+ if 'b' not in filename.mode:
+ raise OpenModeError("File-object must be opened in binary mode")
+
+ try:
+ archive = ZipFile(filename, 'r', ZIP_DEFLATED)
+ except (BadZipfile, RuntimeError, IOError, ValueError):
+ raise InvalidFileException()
+ wb = Workbook()
+
+ if use_iterators:
+ wb._set_optimized_read()
+
+ try:
+ _load_workbook(wb, archive, filename, use_iterators)
+ except KeyError:
+ raise InvalidFileException()
+ finally:
+ archive.close()
+ return wb
+
+def _load_workbook(wb, archive, filename, use_iterators):
+
+ # get workbook-level information
+ wb.properties = read_properties_core(archive.read(ARC_CORE))
+ try:
+ string_table = read_string_table(archive.read(ARC_SHARED_STRINGS))
+ except KeyError:
+ string_table = {}
+ style_table = read_style_table(archive.read(ARC_STYLE))
+
+ # get worksheets
+ wb.worksheets = [] # remove preset worksheet
+ sheet_names = read_sheets_titles(archive.read(ARC_APP))
+ for i, sheet_name in enumerate(sheet_names):
+ sheet_codename = 'sheet%d.xml' % (i + 1)
+ worksheet_path = '%s/%s' % (PACKAGE_WORKSHEETS, sheet_codename)
+
+ if not use_iterators:
+ new_ws = read_worksheet(archive.read(worksheet_path), wb, sheet_name, string_table, style_table)
+ else:
+ xml_source = unpack_worksheet(archive, worksheet_path)
+ new_ws = read_worksheet(xml_source, wb, sheet_name, string_table, style_table, filename, sheet_codename)
+ #new_ws = read_worksheet(archive.read(worksheet_path), wb, sheet_name, string_table, style_table, filename, sheet_codename)
+ wb.add_sheet(new_ws, index = i)
+
+ wb._named_ranges = read_named_ranges(archive.read(ARC_WORKBOOK), wb)
diff --git a/tablib/packages/openpyxl/reader/iter_worksheet.py b/tablib/packages/openpyxl/reader/iter_worksheet.py
new file mode 100644
index 0000000..46ee318
--- /dev/null
+++ b/tablib/packages/openpyxl/reader/iter_worksheet.py
@@ -0,0 +1,348 @@
+# file openpyxl/reader/iter_worksheet.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+""" Iterators-based worksheet reader
+*Still very raw*
+"""
+
+from ....compat import BytesIO as StringIO
+import warnings
+import operator
+from functools import partial
+from itertools import groupby, ifilter
+from ..worksheet import Worksheet
+from ..cell import coordinate_from_string, get_column_letter, Cell
+from ..reader.excel import get_sheet_ids
+from ..reader.strings import read_string_table
+from ..reader.style import read_style_table, NumberFormat
+from ..shared.date_time import SharedDate
+from ..reader.worksheet import read_dimension
+from ..shared.ooxml import (MIN_COLUMN, MAX_COLUMN, PACKAGE_WORKSHEETS,
+ MAX_ROW, MIN_ROW, ARC_SHARED_STRINGS, ARC_APP, ARC_STYLE)
+try:
+ from xml.etree.cElementTree import iterparse
+except ImportError:
+ from xml.etree.ElementTree import iterparse
+
+
+from zipfile import ZipFile
+from .. import cell
+import re
+import tempfile
+import zlib
+import zipfile
+import struct
+
+TYPE_NULL = Cell.TYPE_NULL
+MISSING_VALUE = None
+
+RE_COORDINATE = re.compile('^([A-Z]+)([0-9]+)$')
+
+SHARED_DATE = SharedDate()
+
+_COL_CONVERSION_CACHE = dict((get_column_letter(i), i) for i in xrange(1, 18279))
+def column_index_from_string(str_col, _col_conversion_cache=_COL_CONVERSION_CACHE):
+ # we use a function argument to get indexed name lookup
+ return _col_conversion_cache[str_col]
+del _COL_CONVERSION_CACHE
+
+RAW_ATTRIBUTES = ['row', 'column', 'coordinate', 'internal_value', 'data_type', 'style_id', 'number_format']
+
+try:
+ from collections import namedtuple
+ BaseRawCell = namedtuple('RawCell', RAW_ATTRIBUTES)
+except ImportError:
+
+ # warnings.warn("""Unable to import 'namedtuple' module, this may cause memory issues when using optimized reader. Please upgrade your Python installation to 2.6+""")
+
+ class BaseRawCell(object):
+
+ def __init__(self, *args):
+ assert len(args)==len(RAW_ATTRIBUTES)
+
+ for attr, val in zip(RAW_ATTRIBUTES, args):
+ setattr(self, attr, val)
+
+ def _replace(self, **kwargs):
+
+ self.__dict__.update(kwargs)
+
+ return self
+
+
+class RawCell(BaseRawCell):
+ """Optimized version of the :class:`openpyxl.cell.Cell`, using named tuples.
+
+ Useful attributes are:
+
+ * row
+ * column
+ * coordinate
+ * internal_value
+
+ You can also access if needed:
+
+ * data_type
+ * number_format
+
+ """
+
+ @property
+ def is_date(self):
+ res = (self.data_type == Cell.TYPE_NUMERIC
+ and self.number_format is not None
+ and ('d' in self.number_format
+ or 'm' in self.number_format
+ or 'y' in self.number_format
+ or 'h' in self.number_format
+ or 's' in self.number_format
+ ))
+
+ return res
+
+def iter_rows(workbook_name, sheet_name, xml_source, range_string = '', row_offset = 0, column_offset = 0):
+
+ archive = get_archive_file(workbook_name)
+
+ source = xml_source
+
+ if range_string:
+ min_col, min_row, max_col, max_row = get_range_boundaries(range_string, row_offset, column_offset)
+ else:
+ min_col, min_row, max_col, max_row = read_dimension(xml_source = source)
+ min_col = column_index_from_string(min_col)
+ max_col = column_index_from_string(max_col) + 1
+ max_row += 6
+
+ try:
+ string_table = read_string_table(archive.read(ARC_SHARED_STRINGS))
+ except KeyError:
+ string_table = {}
+
+ style_table = read_style_table(archive.read(ARC_STYLE))
+
+ source.seek(0)
+ p = iterparse(source)
+
+ return get_squared_range(p, min_col, min_row, max_col, max_row, string_table, style_table)
+
+
+def get_rows(p, min_column = MIN_COLUMN, min_row = MIN_ROW, max_column = MAX_COLUMN, max_row = MAX_ROW):
+
+ return groupby(get_cells(p, min_row, min_column, max_row, max_column), operator.attrgetter('row'))
+
+def get_cells(p, min_row, min_col, max_row, max_col, _re_coordinate=RE_COORDINATE):
+
+ for _event, element in p:
+
+ if element.tag == '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}c':
+ coord = element.get('r')
+ column_str, row = _re_coordinate.match(coord).groups()
+
+ row = int(row)
+ column = column_index_from_string(column_str)
+
+ if min_col <= column <= max_col and min_row <= row <= max_row:
+ data_type = element.get('t', 'n')
+ style_id = element.get('s')
+ value = element.findtext('{http://schemas.openxmlformats.org/spreadsheetml/2006/main}v')
+ yield RawCell(row, column_str, coord, value, data_type, style_id, None)
+
+ if element.tag == '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}v':
+ continue
+ element.clear()
+
+
+
+def get_range_boundaries(range_string, row = 0, column = 0):
+
+ if ':' in range_string:
+ min_range, max_range = range_string.split(':')
+ min_col, min_row = coordinate_from_string(min_range)
+ max_col, max_row = coordinate_from_string(max_range)
+
+ min_col = column_index_from_string(min_col) + column
+ max_col = column_index_from_string(max_col) + column
+ min_row += row
+ max_row += row
+
+ else:
+ min_col, min_row = coordinate_from_string(range_string)
+ min_col = column_index_from_string(min_col)
+ max_col = min_col + 1
+ max_row = min_row
+
+ return (min_col, min_row, max_col, max_row)
+
+def get_archive_file(archive_name):
+
+ return ZipFile(archive_name, 'r')
+
+def get_xml_source(archive_file, sheet_name):
+
+ return archive_file.read('%s/%s' % (PACKAGE_WORKSHEETS, sheet_name))
+
+def get_missing_cells(row, columns):
+
+ return dict([(column, RawCell(row, column, '%s%s' % (column, row), MISSING_VALUE, TYPE_NULL, None, None)) for column in columns])
+
+def get_squared_range(p, min_col, min_row, max_col, max_row, string_table, style_table):
+
+ expected_columns = [get_column_letter(ci) for ci in xrange(min_col, max_col)]
+
+ current_row = min_row
+ for row, cells in get_rows(p, min_row = min_row, max_row = max_row, min_column = min_col, max_column = max_col):
+ full_row = []
+ if current_row < row:
+
+ for gap_row in xrange(current_row, row):
+
+ dummy_cells = get_missing_cells(gap_row, expected_columns)
+
+ yield tuple([dummy_cells[column] for column in expected_columns])
+
+ current_row = row
+
+ temp_cells = list(cells)
+
+ retrieved_columns = dict([(c.column, c) for c in temp_cells])
+
+ missing_columns = list(set(expected_columns) - set(retrieved_columns.keys()))
+
+ replacement_columns = get_missing_cells(row, missing_columns)
+
+ for column in expected_columns:
+
+ if column in retrieved_columns:
+ cell = retrieved_columns[column]
+
+ if cell.style_id is not None:
+ style = style_table[int(cell.style_id)]
+ cell = cell._replace(number_format = style.number_format.format_code) #pylint: disable-msg=W0212
+ if cell.internal_value is not None:
+ if cell.data_type == Cell.TYPE_STRING:
+ cell = cell._replace(internal_value = string_table[int(cell.internal_value)]) #pylint: disable-msg=W0212
+ elif cell.data_type == Cell.TYPE_BOOL:
+ cell = cell._replace(internal_value = cell.internal_value == 'True')
+ elif cell.is_date:
+ cell = cell._replace(internal_value = SHARED_DATE.from_julian(float(cell.internal_value)))
+ elif cell.data_type == Cell.TYPE_NUMERIC:
+ cell = cell._replace(internal_value = float(cell.internal_value))
+ full_row.append(cell)
+
+ else:
+ full_row.append(replacement_columns[column])
+
+ current_row = row + 1
+
+ yield tuple(full_row)
+
+#------------------------------------------------------------------------------
+
+class IterableWorksheet(Worksheet):
+
+ def __init__(self, parent_workbook, title, workbook_name,
+ sheet_codename, xml_source):
+
+ Worksheet.__init__(self, parent_workbook, title)
+ self._workbook_name = workbook_name
+ self._sheet_codename = sheet_codename
+ self._xml_source = xml_source
+
+ def iter_rows(self, range_string = '', row_offset = 0, column_offset = 0):
+ """ Returns a squared range based on the `range_string` parameter,
+ using generators.
+
+ :param range_string: range of cells (e.g. 'A1:C4')
+ :type range_string: string
+
+ :param row: row index of the cell (e.g. 4)
+ :type row: int
+
+ :param column: column index of the cell (e.g. 3)
+ :type column: int
+
+ :rtype: generator
+
+ """
+
+ return iter_rows(workbook_name = self._workbook_name,
+ sheet_name = self._sheet_codename,
+ xml_source = self._xml_source,
+ range_string = range_string,
+ row_offset = row_offset,
+ column_offset = column_offset)
+
+ def cell(self, *args, **kwargs):
+
+ raise NotImplementedError("use 'iter_rows()' instead")
+
+ def range(self, *args, **kwargs):
+
+ raise NotImplementedError("use 'iter_rows()' instead")
+
+def unpack_worksheet(archive, filename):
+
+ temp_file = tempfile.TemporaryFile(mode='r+', prefix='openpyxl.', suffix='.unpack.temp')
+
+ zinfo = archive.getinfo(filename)
+
+ if zinfo.compress_type == zipfile.ZIP_STORED:
+ decoder = None
+ elif zinfo.compress_type == zipfile.ZIP_DEFLATED:
+ decoder = zlib.decompressobj(-zlib.MAX_WBITS)
+ else:
+ raise zipfile.BadZipFile("Unrecognized compression method")
+
+ archive.fp.seek(_get_file_offset(archive, zinfo))
+ bytes_to_read = zinfo.compress_size
+
+ while True:
+ buff = archive.fp.read(min(bytes_to_read, 102400))
+ if not buff:
+ break
+ bytes_to_read -= len(buff)
+ if decoder:
+ buff = decoder.decompress(buff)
+ temp_file.write(buff)
+
+ if decoder:
+ temp_file.write(decoder.decompress('Z'))
+
+ return temp_file
+
+def _get_file_offset(archive, zinfo):
+
+ try:
+ return zinfo.file_offset
+ except AttributeError:
+ # From http://stackoverflow.com/questions/3781261/how-to-simulate-zipfile-open-in-python-2-5
+
+ # Seek over the fixed size fields to the "file name length" field in
+ # the file header (26 bytes). Unpack this and the "extra field length"
+ # field ourselves as info.extra doesn't seem to be the correct length.
+ archive.fp.seek(zinfo.header_offset + 26)
+ file_name_len, extra_len = struct.unpack("<HH", archive.fp.read(4))
+ return zinfo.header_offset + 30 + file_name_len + extra_len
diff --git a/tablib/packages/openpyxl/reader/strings.py b/tablib/packages/openpyxl/reader/strings.py
new file mode 100644
index 0000000..e19e291
--- /dev/null
+++ b/tablib/packages/openpyxl/reader/strings.py
@@ -0,0 +1,64 @@
+# file openpyxl/reader/strings.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Read the shared strings table."""
+
+# package imports
+from ..shared.xmltools import fromstring, QName
+from ..shared.ooxml import NAMESPACES
+
+
+def read_string_table(xml_source):
+ """Read in all shared strings in the table"""
+ table = {}
+ xmlns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
+ root = fromstring(text=xml_source)
+ string_index_nodes = root.findall(QName(xmlns, 'si').text)
+ for index, string_index_node in enumerate(string_index_nodes):
+ table[index] = get_string(xmlns, string_index_node)
+ return table
+
+
+def get_string(xmlns, string_index_node):
+ """Read the contents of a specific string index"""
+ rich_nodes = string_index_node.findall(QName(xmlns, 'r').text)
+ if rich_nodes:
+ reconstructed_text = []
+ for rich_node in rich_nodes:
+ partial_text = get_text(xmlns, rich_node)
+ reconstructed_text.append(partial_text)
+ return ''.join(reconstructed_text)
+ else:
+ return get_text(xmlns, string_index_node)
+
+
+def get_text(xmlns, rich_node):
+ """Read rich text, discarding formatting if not disallowed"""
+ text_node = rich_node.find(QName(xmlns, 't').text)
+ partial_text = text_node.text or ''
+
+ if text_node.get(QName(NAMESPACES['xml'], 'space').text) != 'preserve':
+ partial_text = partial_text.strip()
+ return unicode(partial_text)
diff --git a/tablib/packages/openpyxl/reader/style.py b/tablib/packages/openpyxl/reader/style.py
new file mode 100644
index 0000000..f773070
--- /dev/null
+++ b/tablib/packages/openpyxl/reader/style.py
@@ -0,0 +1,69 @@
+# file openpyxl/reader/style.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Read shared style definitions"""
+
+# package imports
+from ..shared.xmltools import fromstring, QName
+from ..shared.exc import MissingNumberFormat
+from ..style import Style, NumberFormat
+
+
+def read_style_table(xml_source):
+ """Read styles from the shared style table"""
+ table = {}
+ xmlns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
+ root = fromstring(xml_source)
+ custom_num_formats = parse_custom_num_formats(root, xmlns)
+ builtin_formats = NumberFormat._BUILTIN_FORMATS
+ cell_xfs = root.find(QName(xmlns, 'cellXfs').text)
+ cell_xfs_nodes = cell_xfs.findall(QName(xmlns, 'xf').text)
+ for index, cell_xfs_node in enumerate(cell_xfs_nodes):
+ new_style = Style()
+ number_format_id = int(cell_xfs_node.get('numFmtId'))
+ if number_format_id < 164:
+ new_style.number_format.format_code = \
+ builtin_formats.get(number_format_id, 'General')
+ else:
+
+ if number_format_id in custom_num_formats:
+ new_style.number_format.format_code = \
+ custom_num_formats[number_format_id]
+ else:
+ raise MissingNumberFormat('%s' % number_format_id)
+ table[index] = new_style
+ return table
+
+
+def parse_custom_num_formats(root, xmlns):
+ """Read in custom numeric formatting rules from the shared style table"""
+ custom_formats = {}
+ num_fmts = root.find(QName(xmlns, 'numFmts').text)
+ if num_fmts is not None:
+ num_fmt_nodes = num_fmts.findall(QName(xmlns, 'numFmt').text)
+ for num_fmt_node in num_fmt_nodes:
+ custom_formats[int(num_fmt_node.get('numFmtId'))] = \
+ num_fmt_node.get('formatCode')
+ return custom_formats
diff --git a/tablib/packages/openpyxl/reader/workbook.py b/tablib/packages/openpyxl/reader/workbook.py
new file mode 100644
index 0000000..d9bc161
--- /dev/null
+++ b/tablib/packages/openpyxl/reader/workbook.py
@@ -0,0 +1,156 @@
+# file openpyxl/reader/workbook.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Read in global settings to be maintained by the workbook object."""
+
+# package imports
+from ..shared.xmltools import fromstring, QName
+from ..shared.ooxml import NAMESPACES
+from ..workbook import DocumentProperties
+from ..shared.date_time import W3CDTF_to_datetime
+from ..namedrange import NamedRange, split_named_range
+
+import datetime
+
+# constants
+BUGGY_NAMED_RANGES = ['NA()', '#REF!']
+DISCARDED_RANGES = ['Excel_BuiltIn', 'Print_Area']
+
+def get_sheet_ids(xml_source):
+
+ sheet_names = read_sheets_titles(xml_source)
+
+ return dict((sheet, 'sheet%d.xml' % (i + 1)) for i, sheet in enumerate(sheet_names))
+
+
+def read_properties_core(xml_source):
+ """Read assorted file properties."""
+ properties = DocumentProperties()
+ root = fromstring(xml_source)
+ creator_node = root.find(QName(NAMESPACES['dc'], 'creator').text)
+ if creator_node is not None:
+ properties.creator = creator_node.text
+ else:
+ properties.creator = ''
+ last_modified_by_node = root.find(
+ QName(NAMESPACES['cp'], 'lastModifiedBy').text)
+ if last_modified_by_node is not None:
+ properties.last_modified_by = last_modified_by_node.text
+ else:
+ properties.last_modified_by = ''
+
+ created_node = root.find(QName(NAMESPACES['dcterms'], 'created').text)
+ if created_node is not None:
+ properties.created = W3CDTF_to_datetime(created_node.text)
+ else:
+ properties.created = datetime.datetime.now()
+
+ modified_node = root.find(QName(NAMESPACES['dcterms'], 'modified').text)
+ if modified_node is not None:
+ properties.modified = W3CDTF_to_datetime(modified_node.text)
+ else:
+ properties.modified = properties.created
+
+ return properties
+
+
+def get_number_of_parts(xml_source):
+ """Get a list of contents of the workbook."""
+ parts_size = {}
+ parts_names = []
+ root = fromstring(xml_source)
+ heading_pairs = root.find(QName('http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
+ 'HeadingPairs').text)
+ vector = heading_pairs.find(QName(NAMESPACES['vt'], 'vector').text)
+ children = vector.getchildren()
+ for child_id in range(0, len(children), 2):
+ part_name = children[child_id].find(QName(NAMESPACES['vt'],
+ 'lpstr').text).text
+ if not part_name in parts_names:
+ parts_names.append(part_name)
+ part_size = int(children[child_id + 1].find(QName(
+ NAMESPACES['vt'], 'i4').text).text)
+ parts_size[part_name] = part_size
+ return parts_size, parts_names
+
+
+def read_sheets_titles(xml_source):
+ """Read titles for all sheets."""
+ root = fromstring(xml_source)
+ titles_root = root.find(QName('http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
+ 'TitlesOfParts').text)
+ vector = titles_root.find(QName(NAMESPACES['vt'], 'vector').text)
+ parts, names = get_number_of_parts(xml_source)
+
+ # we can't assume 'Worksheets' to be written in english,
+ # but it's always the first item of the parts list (see bug #22)
+ size = parts[names[0]]
+ children = [c.text for c in vector.getchildren()]
+ return children[:size]
+
+
+def read_named_ranges(xml_source, workbook):
+ """Read named ranges, excluding poorly defined ranges."""
+ named_ranges = []
+ root = fromstring(xml_source)
+ names_root = root.find(QName('http://schemas.openxmlformats.org/spreadsheetml/2006/main',
+ 'definedNames').text)
+ if names_root is not None:
+
+ for name_node in names_root.getchildren():
+ range_name = name_node.get('name')
+
+ if name_node.get("hidden", '0') == '1':
+ continue
+
+ valid = True
+
+ for discarded_range in DISCARDED_RANGES:
+ if discarded_range in range_name:
+ valid = False
+
+ for bad_range in BUGGY_NAMED_RANGES:
+ if bad_range in name_node.text:
+ valid = False
+
+ if valid:
+ destinations = split_named_range(name_node.text)
+
+ new_destinations = []
+ for worksheet, cells_range in destinations:
+
+ # it can happen that a valid named range references
+ # a missing worksheet, when Excel didn't properly maintain
+ # the named range list
+ #
+ # we just ignore them here
+ worksheet = workbook.get_sheet_by_name(worksheet)
+ if worksheet:
+ new_destinations.append((worksheet, cells_range))
+
+ named_range = NamedRange(range_name, new_destinations)
+ named_ranges.append(named_range)
+
+ return named_ranges
diff --git a/tablib/packages/openpyxl/reader/worksheet.py b/tablib/packages/openpyxl/reader/worksheet.py
new file mode 100644
index 0000000..a14c4a8
--- /dev/null
+++ b/tablib/packages/openpyxl/reader/worksheet.py
@@ -0,0 +1,114 @@
+# file openpyxl/reader/worksheet.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Reader for a single worksheet."""
+
+# Python stdlib imports
+try:
+ from xml.etree.cElementTree import iterparse
+except ImportError:
+ from xml.etree.ElementTree import iterparse
+
+from ....compat import ifilter
+from ....compat import BytesIO as StringIO
+
+# package imports
+from ..cell import Cell, coordinate_from_string
+from ..worksheet import Worksheet
+
+def _get_xml_iter(xml_source):
+
+ if not hasattr(xml_source, 'name'):
+ return StringIO(xml_source)
+ else:
+ xml_source.seek(0)
+ return xml_source
+
+def read_dimension(xml_source):
+
+ source = _get_xml_iter(xml_source)
+
+ it = iterparse(source)
+
+ for event, element in it:
+
+ if element.tag == '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}dimension':
+ ref = element.get('ref')
+
+ min_range, max_range = ref.split(':')
+ min_col, min_row = coordinate_from_string(min_range)
+ max_col, max_row = coordinate_from_string(max_range)
+
+ return min_col, min_row, max_col, max_row
+
+ else:
+ element.clear()
+
+ return None
+
+def filter_cells(x):
+ (event, element) = x
+
+ return element.tag == '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}c'
+
+def fast_parse(ws, xml_source, string_table, style_table):
+
+ source = _get_xml_iter(xml_source)
+
+ it = iterparse(source)
+
+ for event, element in ifilter(filter_cells, it):
+
+ value = element.findtext('{http://schemas.openxmlformats.org/spreadsheetml/2006/main}v')
+
+ if value is not None:
+
+ coordinate = element.get('r')
+ data_type = element.get('t', 'n')
+ style_id = element.get('s')
+
+ if data_type == Cell.TYPE_STRING:
+ value = string_table.get(int(value))
+
+ ws.cell(coordinate).value = value
+
+ if style_id is not None:
+ ws._styles[coordinate] = style_table.get(int(style_id))
+
+ # to avoid memory exhaustion, clear the item after use
+ element.clear()
+
+from ..reader.iter_worksheet import IterableWorksheet
+
+def read_worksheet(xml_source, parent, preset_title, string_table,
+ style_table, workbook_name = None, sheet_codename = None):
+ """Read an xml worksheet"""
+ if workbook_name and sheet_codename:
+ ws = IterableWorksheet(parent, preset_title, workbook_name,
+ sheet_codename, xml_source)
+ else:
+ ws = Worksheet(parent, preset_title)
+ fast_parse(ws, xml_source, string_table, style_table)
+ return ws
diff --git a/tablib/packages/openpyxl/shared/__init__.py b/tablib/packages/openpyxl/shared/__init__.py
new file mode 100644
index 0000000..8b560df
--- /dev/null
+++ b/tablib/packages/openpyxl/shared/__init__.py
@@ -0,0 +1,33 @@
+# file openpyxl/shared/__init__.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Imports for the openpyxl.shared namespace."""
+
+# package imports
+from . import date_time
+from . import exc
+from . import ooxml
+from . import password_hasher
+from . import xmltools
diff --git a/tablib/packages/openpyxl/shared/date_time.py b/tablib/packages/openpyxl/shared/date_time.py
new file mode 100644
index 0000000..7d87788
--- /dev/null
+++ b/tablib/packages/openpyxl/shared/date_time.py
@@ -0,0 +1,154 @@
+# file openpyxl/shared/date_time.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Manage Excel date weirdness."""
+
+# Python stdlib imports
+from __future__ import division
+from math import floor
+import calendar
+import datetime
+import time
+import re
+
+# constants
+W3CDTF_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+
+RE_W3CDTF = '(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(.(\d{2}))?Z'
+
+EPOCH = datetime.datetime.utcfromtimestamp(0)
+
+def datetime_to_W3CDTF(dt):
+ """Convert from a datetime to a timestamp string."""
+ return datetime.datetime.strftime(dt, W3CDTF_FORMAT)
+
+
+def W3CDTF_to_datetime(formatted_string):
+ """Convert from a timestamp string to a datetime object."""
+ match = re.match(RE_W3CDTF,formatted_string)
+ digits = map(int, match.groups()[:6])
+ return datetime.datetime(*digits)
+
+
+class SharedDate(object):
+ """Date formatting utilities for Excel with shared state.
+
+ Excel has a two primary date tracking schemes:
+ Windows - Day 1 == 1900-01-01
+ Mac - Day 1 == 1904-01-01
+
+ SharedDate stores which system we are using and converts dates between
+ Python and Excel accordingly.
+
+ """
+ CALENDAR_WINDOWS_1900 = 1900
+ CALENDAR_MAC_1904 = 1904
+ datetime_object_type = 'DateTime'
+
+ def __init__(self):
+ self.excel_base_date = self.CALENDAR_WINDOWS_1900
+
+ def datetime_to_julian(self, date):
+ """Convert from python datetime to excel julian date representation."""
+
+ if isinstance(date, datetime.datetime):
+ return self.to_julian(date.year, date.month, date.day, \
+ hours=date.hour, minutes=date.minute, seconds=date.second)
+ elif isinstance(date, datetime.date):
+ return self.to_julian(date.year, date.month, date.day)
+
+ def to_julian(self, year, month, day, hours=0, minutes=0, seconds=0):
+ """Convert from Python date to Excel JD."""
+ # explicitly disallow bad years
+ # Excel 2000 treats JD=0 as 1/0/1900 (buggy, disallow)
+ # Excel 2000 treats JD=2958466 as a bad date (Y10K bug!)
+ if year < 1900 or year > 10000:
+ msg = 'Year not supported by Excel: %s' % year
+ raise ValueError(msg)
+ if self.excel_base_date == self.CALENDAR_WINDOWS_1900:
+ # Fudge factor for the erroneous fact that the year 1900 is
+ # treated as a Leap Year in MS Excel. This affects every date
+ # following 28th February 1900
+ if year == 1900 and month <= 2:
+ excel_1900_leap_year = False
+ else:
+ excel_1900_leap_year = True
+ excel_base_date = 2415020
+ else:
+ raise NotImplementedError('Mac dates are not yet supported.')
+ #excel_base_date = 2416481
+ #excel_1900_leap_year = False
+
+ # Julian base date adjustment
+ if month > 2:
+ month = month - 3
+ else:
+ month = month + 9
+ year -= 1
+
+ # Calculate the Julian Date, then subtract the Excel base date
+ # JD 2415020 = 31 - Dec - 1899 -> Excel Date of 0
+ century, decade = int(str(year)[:2]), int(str(year)[2:])
+ excel_date = floor(146097 * century / 4) + \
+ floor((1461 * decade) / 4) + floor((153 * month + 2) / 5) + \
+ day + 1721119 - excel_base_date
+ if excel_1900_leap_year:
+ excel_date += 1
+
+ # check to ensure that we exclude 2/29/1900 as a possible value
+ if self.excel_base_date == self.CALENDAR_WINDOWS_1900 \
+ and excel_date == 60:
+ msg = 'Error: Excel believes 1900 was a leap year'
+ raise ValueError(msg)
+ excel_time = ((hours * 3600) + (minutes * 60) + seconds) / 86400
+ return excel_date + excel_time
+
+ def from_julian(self, value=0):
+ """Convert from the Excel JD back to a date"""
+ if self.excel_base_date == self.CALENDAR_WINDOWS_1900:
+ excel_base_date = 25569
+ if value < 60:
+ excel_base_date -= 1
+ elif value == 60:
+ msg = 'Error: Excel believes 1900 was a leap year'
+ raise ValueError(msg)
+ else:
+ raise NotImplementedError('Mac dates are not yet supported.')
+ #excel_base_date = 24107
+
+ if value >= 1:
+ utc_days = value - excel_base_date
+
+ return EPOCH + datetime.timedelta(days=utc_days)
+
+ elif value >= 0:
+ hours = floor(value * 24)
+ mins = floor(value * 24 * 60) - floor(hours * 60)
+ secs = floor(value * 24 * 60 * 60) - floor(hours * 60 * 60) - \
+ floor(mins * 60)
+ return datetime.time(int(hours), int(mins), int(secs))
+ else:
+ msg = 'Negative dates (%s) are not supported' % value
+ raise ValueError(msg)
diff --git a/tablib/packages/openpyxl/shared/exc.py b/tablib/packages/openpyxl/shared/exc.py
new file mode 100644
index 0000000..94a3e2c
--- /dev/null
+++ b/tablib/packages/openpyxl/shared/exc.py
@@ -0,0 +1,59 @@
+# file openpyxl/shared/exc.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Definitions for openpyxl shared exception classes."""
+
+
+class CellCoordinatesException(Exception):
+ """Error for converting between numeric and A1-style cell references."""
+
+class ColumnStringIndexException(Exception):
+ """Error for bad column names in A1-style cell references."""
+
+class DataTypeException(Exception):
+ """Error for any data type inconsistencies."""
+
+class NamedRangeException(Exception):
+ """Error for badly formatted named ranges."""
+
+class SheetTitleException(Exception):
+ """Error for bad sheet names."""
+
+class InsufficientCoordinatesException(Exception):
+ """Error for partially specified cell coordinates."""
+
+class OpenModeError(Exception):
+ """Error for fileobj opened in non-binary mode."""
+
+class InvalidFileException(Exception):
+ """Error for trying to open a non-ooxml file."""
+
+class ReadOnlyWorkbookException(Exception):
+ """Error for trying to modify a read-only workbook"""
+
+class MissingNumberFormat(Exception):
+ """Error when a referenced number format is not in the stylesheet"""
+
+
diff --git a/tablib/packages/openpyxl/shared/ooxml.py b/tablib/packages/openpyxl/shared/ooxml.py
new file mode 100644
index 0000000..979b172
--- /dev/null
+++ b/tablib/packages/openpyxl/shared/ooxml.py
@@ -0,0 +1,60 @@
+# file openpyxl/shared/ooxml.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Constants for fixed paths in a file and xml namespace urls."""
+
+MIN_ROW = 0
+MIN_COLUMN = 0
+MAX_COLUMN = 16384
+MAX_ROW = 1048576
+
+# constants
+PACKAGE_PROPS = 'docProps'
+PACKAGE_XL = 'xl'
+PACKAGE_RELS = '_rels'
+PACKAGE_THEME = PACKAGE_XL + '/' + 'theme'
+PACKAGE_WORKSHEETS = PACKAGE_XL + '/' + 'worksheets'
+PACKAGE_DRAWINGS = PACKAGE_XL + '/' + 'drawings'
+PACKAGE_CHARTS = PACKAGE_XL + '/' + 'charts'
+
+ARC_CONTENT_TYPES = '[Content_Types].xml'
+ARC_ROOT_RELS = PACKAGE_RELS + '/.rels'
+ARC_WORKBOOK_RELS = PACKAGE_XL + '/' + PACKAGE_RELS + '/workbook.xml.rels'
+ARC_CORE = PACKAGE_PROPS + '/core.xml'
+ARC_APP = PACKAGE_PROPS + '/app.xml'
+ARC_WORKBOOK = PACKAGE_XL + '/workbook.xml'
+ARC_STYLE = PACKAGE_XL + '/styles.xml'
+ARC_THEME = PACKAGE_THEME + '/theme1.xml'
+ARC_SHARED_STRINGS = PACKAGE_XL + '/sharedStrings.xml'
+
+NAMESPACES = {
+ 'cp': 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties',
+ 'dc': 'http://purl.org/dc/elements/1.1/',
+ 'dcterms': 'http://purl.org/dc/terms/',
+ 'dcmitype': 'http://purl.org/dc/dcmitype/',
+ 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
+ 'vt': 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes',
+ 'xml': 'http://www.w3.org/XML/1998/namespace'
+}
diff --git a/tablib/packages/openpyxl/shared/password_hasher.py b/tablib/packages/openpyxl/shared/password_hasher.py
new file mode 100644
index 0000000..b5d0dd0
--- /dev/null
+++ b/tablib/packages/openpyxl/shared/password_hasher.py
@@ -0,0 +1,47 @@
+# file openpyxl/shared/password_hasher.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Basic password hashing."""
+
+
+def hash_password(plaintext_password=''):
+ """Create a password hash from a given string.
+
+ This method is based on the algorithm provided by
+ Daniel Rentz of OpenOffice and the PEAR package
+ Spreadsheet_Excel_Writer by Xavier Noguer <xnoguer@rezebra.com>.
+
+ """
+ password = 0x0000
+ i = 1
+ for char in plaintext_password:
+ value = ord(char) << i
+ rotated_bits = value >> 15
+ value &= 0x7fff
+ password ^= (value | rotated_bits)
+ i += 1
+ password ^= len(plaintext_password)
+ password ^= 0xCE4B
+ return str(hex(password)).upper()[2:]
diff --git a/tablib/packages/openpyxl/shared/units.py b/tablib/packages/openpyxl/shared/units.py
new file mode 100644
index 0000000..fba82d7
--- /dev/null
+++ b/tablib/packages/openpyxl/shared/units.py
@@ -0,0 +1,67 @@
+# file openpyxl/shared/units.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+import math
+
+def pixels_to_EMU(value):
+ return int(round(value * 9525))
+
+def EMU_to_pixels(value):
+ if not value:
+ return 0
+ else:
+ return round(value / 9525.)
+
+def EMU_to_cm(value):
+ if not value:
+ return 0
+ else:
+ return (EMU_to_pixels(value) * 2.57 / 96)
+
+def pixels_to_points(value):
+ return value * 0.67777777
+
+def points_to_pixels(value):
+ if not value:
+ return 0
+ else:
+ return int(math.ceil(value * 1.333333333))
+
+def degrees_to_angle(value):
+ return int(round(value * 60000))
+
+def angle_to_degrees(value):
+ if not value:
+ return 0
+ else:
+ return round(value / 60000.)
+
+def short_color(color):
+ """ format a color to its short size """
+
+ if len(color) > 6:
+ return color[2:]
+ else:
+ return color
diff --git a/tablib/packages/openpyxl/shared/xmltools.py b/tablib/packages/openpyxl/shared/xmltools.py
new file mode 100644
index 0000000..74729e9
--- /dev/null
+++ b/tablib/packages/openpyxl/shared/xmltools.py
@@ -0,0 +1,114 @@
+# file openpyxl/shared/xmltools.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Shared xml tools.
+
+Shortcut functions taken from:
+ http://lethain.com/entry/2009/jan/22/handling-very-large-csv-and-xml-files-in-python/
+
+"""
+
+# Python stdlib imports
+from xml.sax.xmlreader import AttributesNSImpl
+from xml.sax.saxutils import XMLGenerator
+try:
+ from xml.etree.ElementTree import ElementTree, Element, SubElement, \
+ QName, fromstring, tostring
+except ImportError:
+ from cElementTree import ElementTree, Element, SubElement, \
+ QName, fromstring, tostring
+
+# package imports
+from .. import __name__ as prefix
+
+
+def get_document_content(xml_node):
+ """Print nicely formatted xml to a string."""
+ pretty_indent(xml_node)
+ return tostring(xml_node, 'utf-8')
+
+
+def pretty_indent(elem, level=0):
+ """Format xml with nice indents and line breaks."""
+ i = "\n" + level * " "
+ if len(elem):
+ if not elem.text or not elem.text.strip():
+ elem.text = i + " "
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ for elem in elem:
+ pretty_indent(elem, level + 1)
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ else:
+ if level and (not elem.tail or not elem.tail.strip()):
+ elem.tail = i
+
+
+def start_tag(doc, name, attr=None, body=None, namespace=None):
+ """Wrapper to start an xml tag."""
+ if attr is None:
+ attr = {}
+
+
+ # name = bytes(name, 'utf-8')
+
+ # if namespace is not None:
+ # namespace = bytes(namespace, 'utf-8')
+
+
+ attr_vals = {}
+ attr_keys = {}
+ for key, val in attr.items():
+
+
+ # if key is not None:
+ # key = bytes(key, 'utf-8')
+
+ # if val is not None:
+ # val = bytes(val, 'utf-8')
+
+ key_tuple = (namespace, key)
+
+ attr_vals[key_tuple] = val
+ attr_keys[key_tuple] = key
+
+ attr2 = AttributesNSImpl(attr_vals, attr_keys)
+ doc.startElementNS((namespace, name), name, attr2)
+ if body:
+ doc.characters(body)
+
+
+def end_tag(doc, name, namespace=None):
+ """Wrapper to close an xml tag."""
+ doc.endElementNS((namespace, name), name)
+
+
+def tag(doc, name, attr=None, body=None, namespace=None):
+ """Wrapper to print xml tags and comments."""
+ if attr is None:
+ attr = {}
+ start_tag(doc, name, attr, body, namespace)
+ end_tag(doc, name, namespace)
diff --git a/tablib/packages/openpyxl/style.py b/tablib/packages/openpyxl/style.py
new file mode 100644
index 0000000..38628db
--- /dev/null
+++ b/tablib/packages/openpyxl/style.py
@@ -0,0 +1,392 @@
+# file openpyxl/style.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Style and formatting option tracking."""
+
+# Python stdlib imports
+import re
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+
+
+class HashableObject(object):
+ """Define how to hash property classes."""
+ __fields__ = None
+ __leaf__ = False
+
+ def __repr__(self):
+
+ return ':'.join([repr(getattr(self, x)) for x in self.__fields__])
+
+ def __hash__(self):
+
+# return int(md5(repr(self)).hexdigest(), 16)
+ return hash(repr(self))
+
+class Color(HashableObject):
+ """Named colors for use in styles."""
+ BLACK = 'FF000000'
+ WHITE = 'FFFFFFFF'
+ RED = 'FFFF0000'
+ DARKRED = 'FF800000'
+ BLUE = 'FF0000FF'
+ DARKBLUE = 'FF000080'
+ GREEN = 'FF00FF00'
+ DARKGREEN = 'FF008000'
+ YELLOW = 'FFFFFF00'
+ DARKYELLOW = 'FF808000'
+
+ __fields__ = ('index',)
+ __slots__ = __fields__
+ __leaf__ = True
+
+ def __init__(self, index):
+ super(Color, self).__init__()
+ self.index = index
+
+
+class Font(HashableObject):
+ """Font options used in styles."""
+ UNDERLINE_NONE = 'none'
+ UNDERLINE_DOUBLE = 'double'
+ UNDERLINE_DOUBLE_ACCOUNTING = 'doubleAccounting'
+ UNDERLINE_SINGLE = 'single'
+ UNDERLINE_SINGLE_ACCOUNTING = 'singleAccounting'
+
+ __fields__ = ('name',
+ 'size',
+ 'bold',
+ 'italic',
+ 'superscript',
+ 'subscript',
+ 'underline',
+ 'strikethrough',
+ 'color')
+ __slots__ = __fields__
+
+ def __init__(self):
+ super(Font, self).__init__()
+ self.name = 'Calibri'
+ self.size = 11
+ self.bold = False
+ self.italic = False
+ self.superscript = False
+ self.subscript = False
+ self.underline = self.UNDERLINE_NONE
+ self.strikethrough = False
+ self.color = Color(Color.BLACK)
+
+
+class Fill(HashableObject):
+ """Area fill patterns for use in styles."""
+ FILL_NONE = 'none'
+ FILL_SOLID = 'solid'
+ FILL_GRADIENT_LINEAR = 'linear'
+ FILL_GRADIENT_PATH = 'path'
+ FILL_PATTERN_DARKDOWN = 'darkDown'
+ FILL_PATTERN_DARKGRAY = 'darkGray'
+ FILL_PATTERN_DARKGRID = 'darkGrid'
+ FILL_PATTERN_DARKHORIZONTAL = 'darkHorizontal'
+ FILL_PATTERN_DARKTRELLIS = 'darkTrellis'
+ FILL_PATTERN_DARKUP = 'darkUp'
+ FILL_PATTERN_DARKVERTICAL = 'darkVertical'
+ FILL_PATTERN_GRAY0625 = 'gray0625'
+ FILL_PATTERN_GRAY125 = 'gray125'
+ FILL_PATTERN_LIGHTDOWN = 'lightDown'
+ FILL_PATTERN_LIGHTGRAY = 'lightGray'
+ FILL_PATTERN_LIGHTGRID = 'lightGrid'
+ FILL_PATTERN_LIGHTHORIZONTAL = 'lightHorizontal'
+ FILL_PATTERN_LIGHTTRELLIS = 'lightTrellis'
+ FILL_PATTERN_LIGHTUP = 'lightUp'
+ FILL_PATTERN_LIGHTVERTICAL = 'lightVertical'
+ FILL_PATTERN_MEDIUMGRAY = 'mediumGray'
+
+ __fields__ = ('fill_type',
+ 'rotation',
+ 'start_color',
+ 'end_color')
+ __slots__ = __fields__
+
+ def __init__(self):
+ super(Fill, self).__init__()
+ self.fill_type = self.FILL_NONE
+ self.rotation = 0
+ self.start_color = Color(Color.WHITE)
+ self.end_color = Color(Color.BLACK)
+
+
+class Border(HashableObject):
+ """Border options for use in styles."""
+ BORDER_NONE = 'none'
+ BORDER_DASHDOT = 'dashDot'
+ BORDER_DASHDOTDOT = 'dashDotDot'
+ BORDER_DASHED = 'dashed'
+ BORDER_DOTTED = 'dotted'
+ BORDER_DOUBLE = 'double'
+ BORDER_HAIR = 'hair'
+ BORDER_MEDIUM = 'medium'
+ BORDER_MEDIUMDASHDOT = 'mediumDashDot'
+ BORDER_MEDIUMDASHDOTDOT = 'mediumDashDotDot'
+ BORDER_MEDIUMDASHED = 'mediumDashed'
+ BORDER_SLANTDASHDOT = 'slantDashDot'
+ BORDER_THICK = 'thick'
+ BORDER_THIN = 'thin'
+
+ __fields__ = ('border_style',
+ 'color')
+ __slots__ = __fields__
+
+ def __init__(self):
+ super(Border, self).__init__()
+ self.border_style = self.BORDER_NONE
+ self.color = Color(Color.BLACK)
+
+
+class Borders(HashableObject):
+ """Border positioning for use in styles."""
+ DIAGONAL_NONE = 0
+ DIAGONAL_UP = 1
+ DIAGONAL_DOWN = 2
+ DIAGONAL_BOTH = 3
+
+ __fields__ = ('left',
+ 'right',
+ 'top',
+ 'bottom',
+ 'diagonal',
+ 'diagonal_direction',
+ 'all_borders',
+ 'outline',
+ 'inside',
+ 'vertical',
+ 'horizontal')
+ __slots__ = __fields__
+
+ def __init__(self):
+ super(Borders, self).__init__()
+ self.left = Border()
+ self.right = Border()
+ self.top = Border()
+ self.bottom = Border()
+ self.diagonal = Border()
+ self.diagonal_direction = self.DIAGONAL_NONE
+
+ self.all_borders = Border()
+ self.outline = Border()
+ self.inside = Border()
+ self.vertical = Border()
+ self.horizontal = Border()
+
+
+class Alignment(HashableObject):
+ """Alignment options for use in styles."""
+ HORIZONTAL_GENERAL = 'general'
+ HORIZONTAL_LEFT = 'left'
+ HORIZONTAL_RIGHT = 'right'
+ HORIZONTAL_CENTER = 'center'
+ HORIZONTAL_CENTER_CONTINUOUS = 'centerContinuous'
+ HORIZONTAL_JUSTIFY = 'justify'
+ VERTICAL_BOTTOM = 'bottom'
+ VERTICAL_TOP = 'top'
+ VERTICAL_CENTER = 'center'
+ VERTICAL_JUSTIFY = 'justify'
+
+ __fields__ = ('horizontal',
+ 'vertical',
+ 'text_rotation',
+ 'wrap_text',
+ 'shrink_to_fit',
+ 'indent')
+ __slots__ = __fields__
+ __leaf__ = True
+
+ def __init__(self):
+ super(Alignment, self).__init__()
+ self.horizontal = self.HORIZONTAL_GENERAL
+ self.vertical = self.VERTICAL_BOTTOM
+ self.text_rotation = 0
+ self.wrap_text = False
+ self.shrink_to_fit = False
+ self.indent = 0
+
+
+class NumberFormat(HashableObject):
+ """Numer formatting for use in styles."""
+ FORMAT_GENERAL = 'General'
+ FORMAT_TEXT = '@'
+ FORMAT_NUMBER = '0'
+ FORMAT_NUMBER_00 = '0.00'
+ FORMAT_NUMBER_COMMA_SEPARATED1 = '#,##0.00'
+ FORMAT_NUMBER_COMMA_SEPARATED2 = '#,##0.00_-'
+ FORMAT_PERCENTAGE = '0%'
+ FORMAT_PERCENTAGE_00 = '0.00%'
+ FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd'
+ FORMAT_DATE_YYYYMMDD = 'yy-mm-dd'
+ FORMAT_DATE_DDMMYYYY = 'dd/mm/yy'
+ FORMAT_DATE_DMYSLASH = 'd/m/y'
+ FORMAT_DATE_DMYMINUS = 'd-m-y'
+ FORMAT_DATE_DMMINUS = 'd-m'
+ FORMAT_DATE_MYMINUS = 'm-y'
+ FORMAT_DATE_XLSX14 = 'mm-dd-yy'
+ FORMAT_DATE_XLSX15 = 'd-mmm-yy'
+ FORMAT_DATE_XLSX16 = 'd-mmm'
+ FORMAT_DATE_XLSX17 = 'mmm-yy'
+ FORMAT_DATE_XLSX22 = 'm/d/yy h:mm'
+ FORMAT_DATE_DATETIME = 'd/m/y h:mm'
+ FORMAT_DATE_TIME1 = 'h:mm AM/PM'
+ FORMAT_DATE_TIME2 = 'h:mm:ss AM/PM'
+ FORMAT_DATE_TIME3 = 'h:mm'
+ FORMAT_DATE_TIME4 = 'h:mm:ss'
+ FORMAT_DATE_TIME5 = 'mm:ss'
+ FORMAT_DATE_TIME6 = 'h:mm:ss'
+ FORMAT_DATE_TIME7 = 'i:s.S'
+ FORMAT_DATE_TIME8 = 'h:mm:ss@'
+ FORMAT_DATE_YYYYMMDDSLASH = 'yy/mm/dd@'
+ FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-'
+ FORMAT_CURRENCY_USD = '$#,##0_-'
+ FORMAT_CURRENCY_EUR_SIMPLE = '[$EUR ]#,##0.00_-'
+ _BUILTIN_FORMATS = {
+ 0: 'General',
+ 1: '0',
+ 2: '0.00',
+ 3: '#,##0',
+ 4: '#,##0.00',
+
+ 9: '0%',
+ 10: '0.00%',
+ 11: '0.00E+00',
+ 12: '# ?/?',
+ 13: '# ??/??',
+ 14: 'mm-dd-yy',
+ 15: 'd-mmm-yy',
+ 16: 'd-mmm',
+ 17: 'mmm-yy',
+ 18: 'h:mm AM/PM',
+ 19: 'h:mm:ss AM/PM',
+ 20: 'h:mm',
+ 21: 'h:mm:ss',
+ 22: 'm/d/yy h:mm',
+
+ 37: '#,##0 (#,##0)',
+ 38: '#,##0 [Red](#,##0)',
+ 39: '#,##0.00(#,##0.00)',
+ 40: '#,##0.00[Red](#,##0.00)',
+
+ 41: '_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)',
+ 42: '_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)',
+ 43: '_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)',
+
+ 44: '_("$"* #,##0.00_)_("$"* \(#,##0.00\)_("$"* "-"??_)_(@_)',
+ 45: 'mm:ss',
+ 46: '[h]:mm:ss',
+ 47: 'mmss.0',
+ 48: '##0.0E+0',
+ 49: '@', }
+ _BUILTIN_FORMATS_REVERSE = dict(
+ [(value, key) for key, value in _BUILTIN_FORMATS.items()])
+
+ __fields__ = ('_format_code',
+ '_format_index')
+ __slots__ = __fields__
+ __leaf__ = True
+
+ DATE_INDICATORS = 'dmyhs'
+
+ def __init__(self):
+ super(NumberFormat, self).__init__()
+ self._format_code = self.FORMAT_GENERAL
+ self._format_index = 0
+
+ def _set_format_code(self, format_code = FORMAT_GENERAL):
+ """Setter for the format_code property."""
+ self._format_code = format_code
+ self._format_index = self.builtin_format_id(format = format_code)
+
+ def _get_format_code(self):
+ """Getter for the format_code property."""
+ return self._format_code
+
+ format_code = property(_get_format_code, _set_format_code)
+
+ def builtin_format_code(self, index):
+ """Return one of the standard format codes by index."""
+ return self._BUILTIN_FORMATS[index]
+
+ def is_builtin(self, format = None):
+ """Check if a format code is a standard format code."""
+ if format is None:
+ format = self._format_code
+ return format in self._BUILTIN_FORMATS.values()
+
+ def builtin_format_id(self, format):
+ """Return the id of a standard style."""
+ return self._BUILTIN_FORMATS_REVERSE.get(format, None)
+
+ def is_date_format(self, format = None):
+ """Check if the number format is actually representing a date."""
+ if format is None:
+ format = self._format_code
+
+ return any([x in format for x in self.DATE_INDICATORS])
+
+class Protection(HashableObject):
+ """Protection options for use in styles."""
+ PROTECTION_INHERIT = 'inherit'
+ PROTECTION_PROTECTED = 'protected'
+ PROTECTION_UNPROTECTED = 'unprotected'
+
+ __fields__ = ('locked',
+ 'hidden')
+ __slots__ = __fields__
+ __leaf__ = True
+
+ def __init__(self):
+ super(Protection, self).__init__()
+ self.locked = self.PROTECTION_INHERIT
+ self.hidden = self.PROTECTION_INHERIT
+
+
+class Style(HashableObject):
+ """Style object containing all formatting details."""
+ __fields__ = ('font',
+ 'fill',
+ 'borders',
+ 'alignment',
+ 'number_format',
+ 'protection')
+ __slots__ = __fields__
+
+ def __init__(self):
+ super(Style, self).__init__()
+ self.font = Font()
+ self.fill = Fill()
+ self.borders = Borders()
+ self.alignment = Alignment()
+ self.number_format = NumberFormat()
+ self.protection = Protection()
+
+DEFAULTS = Style()
diff --git a/tablib/packages/openpyxl/workbook.py b/tablib/packages/openpyxl/workbook.py
new file mode 100644
index 0000000..bbb14b6
--- /dev/null
+++ b/tablib/packages/openpyxl/workbook.py
@@ -0,0 +1,186 @@
+# file openpyxl/workbook.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Workbook is the top-level container for all document information."""
+
+__docformat__ = "restructuredtext en"
+
+# Python stdlib imports
+import datetime
+import os
+
+# package imports
+from .worksheet import Worksheet
+from .writer.dump_worksheet import DumpWorksheet, save_dump
+from .writer.strings import StringTableBuilder
+from .namedrange import NamedRange
+from .style import Style
+from .writer.excel import save_workbook
+from .shared.exc import ReadOnlyWorkbookException
+
+
+class DocumentProperties(object):
+ """High-level properties of the document."""
+
+ def __init__(self):
+ self.creator = 'Unknown'
+ self.last_modified_by = self.creator
+ self.created = datetime.datetime.now()
+ self.modified = datetime.datetime.now()
+ self.title = 'Untitled'
+ self.subject = ''
+ self.description = ''
+ self.keywords = ''
+ self.category = ''
+ self.company = 'Microsoft Corporation'
+
+
+class DocumentSecurity(object):
+ """Security information about the document."""
+
+ def __init__(self):
+ self.lock_revision = False
+ self.lock_structure = False
+ self.lock_windows = False
+ self.revision_password = ''
+ self.workbook_password = ''
+
+
+class Workbook(object):
+ """Workbook is the container for all other parts of the document."""
+
+ def __init__(self, optimized_write = False):
+ self.worksheets = []
+ self._active_sheet_index = 0
+ self._named_ranges = []
+ self.properties = DocumentProperties()
+ self.style = Style()
+ self.security = DocumentSecurity()
+ self.__optimized_write = optimized_write
+ self.__optimized_read = False
+ self.strings_table_builder = StringTableBuilder()
+
+ if not optimized_write:
+ self.worksheets.append(Worksheet(self))
+
+ def _set_optimized_read(self):
+ self.__optimized_read = True
+
+ def get_active_sheet(self):
+ """Returns the current active sheet."""
+ return self.worksheets[self._active_sheet_index]
+
+ def create_sheet(self, index = None):
+ """Create a worksheet (at an optional index).
+
+ :param index: optional position at which the sheet will be inserted
+ :type index: int
+
+ """
+
+ if self.__optimized_read:
+ raise ReadOnlyWorkbookException('Cannot create new sheet in a read-only workbook')
+
+ if self.__optimized_write :
+ new_ws = DumpWorksheet(parent_workbook = self)
+ else:
+ new_ws = Worksheet(parent_workbook = self)
+
+ self.add_sheet(worksheet = new_ws, index = index)
+ return new_ws
+
+ def add_sheet(self, worksheet, index = None):
+ """Add an existing worksheet (at an optional index)."""
+ if index is None:
+ index = len(self.worksheets)
+ self.worksheets.insert(index, worksheet)
+
+ def remove_sheet(self, worksheet):
+ """Remove a worksheet from this workbook."""
+ self.worksheets.remove(worksheet)
+
+ def get_sheet_by_name(self, name):
+ """Returns a worksheet by its name.
+
+ Returns None if no worksheet has the name specified.
+
+ :param name: the name of the worksheet to look for
+ :type name: string
+
+ """
+ requested_sheet = None
+ for sheet in self.worksheets:
+ if sheet.title == name:
+ requested_sheet = sheet
+ break
+ return requested_sheet
+
+ def get_index(self, worksheet):
+ """Return the index of the worksheet."""
+ return self.worksheets.index(worksheet)
+
+ def get_sheet_names(self):
+ """Returns the list of the names of worksheets in the workbook.
+
+ Names are returned in the worksheets order.
+
+ :rtype: list of strings
+
+ """
+ return [s.title for s in self.worksheets]
+
+ def create_named_range(self, name, worksheet, range):
+ """Create a new named_range on a worksheet"""
+ assert isinstance(worksheet, Worksheet)
+ named_range = NamedRange(name, [(worksheet, range)])
+ self.add_named_range(named_range)
+
+ def get_named_ranges(self):
+ """Return all named ranges"""
+ return self._named_ranges
+
+ def add_named_range(self, named_range):
+ """Add an existing named_range to the list of named_ranges."""
+ self._named_ranges.append(named_range)
+
+ def get_named_range(self, name):
+ """Return the range specified by name."""
+ requested_range = None
+ for named_range in self._named_ranges:
+ if named_range.name == name:
+ requested_range = named_range
+ break
+ return requested_range
+
+ def remove_named_range(self, named_range):
+ """Remove a named_range from this workbook."""
+ self._named_ranges.remove(named_range)
+
+ def save(self, filename):
+ """ shortcut """
+ if self.__optimized_write:
+ save_dump(self, filename)
+ else:
+ save_workbook(self, filename)
diff --git a/tablib/packages/openpyxl/worksheet.py b/tablib/packages/openpyxl/worksheet.py
new file mode 100644
index 0000000..4f3955c
--- /dev/null
+++ b/tablib/packages/openpyxl/worksheet.py
@@ -0,0 +1,534 @@
+# file openpyxl/worksheet.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Worksheet is the 2nd-level container in Excel."""
+
+# Python stdlib imports
+import re
+
+# package imports
+from . import cell
+from .cell import coordinate_from_string, \
+ column_index_from_string, get_column_letter
+from .shared.exc import SheetTitleException, \
+ InsufficientCoordinatesException, CellCoordinatesException, \
+ NamedRangeException
+from .shared.password_hasher import hash_password
+from .style import Style, DEFAULTS as DEFAULTS_STYLE
+from .drawing import Drawing
+
+_DEFAULTS_STYLE_HASH = hash(DEFAULTS_STYLE)
+
+def flatten(results):
+
+ rows = []
+
+ for row in results:
+
+ cells = []
+
+ for cell in row:
+
+ cells.append(cell.value)
+
+ rows.append(tuple(cells))
+
+ return tuple(rows)
+
+
+class Relationship(object):
+ """Represents many kinds of relationships."""
+ # TODO: Use this object for workbook relationships as well as
+ # worksheet relationships
+ TYPES = {
+ 'hyperlink': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
+ 'drawing':'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
+ #'worksheet': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
+ #'sharedStrings': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
+ #'styles': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
+ #'theme': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme',
+ }
+
+ def __init__(self, rel_type):
+ if rel_type not in self.TYPES:
+ raise ValueError("Invalid relationship type %s" % rel_type)
+ self.type = self.TYPES[rel_type]
+ self.target = ""
+ self.target_mode = ""
+ self.id = ""
+
+
+class PageSetup(object):
+ """Information about page layout for this sheet"""
+ pass
+
+
+class HeaderFooter(object):
+ """Information about the header/footer for this sheet."""
+ pass
+
+
+class SheetView(object):
+ """Information about the visible portions of this sheet."""
+ pass
+
+
+class RowDimension(object):
+ """Information about the display properties of a row."""
+ __slots__ = ('row_index',
+ 'height',
+ 'visible',
+ 'outline_level',
+ 'collapsed',
+ 'style_index',)
+
+ def __init__(self, index = 0):
+ self.row_index = index
+ self.height = -1
+ self.visible = True
+ self.outline_level = 0
+ self.collapsed = False
+ self.style_index = None
+
+
+class ColumnDimension(object):
+ """Information about the display properties of a column."""
+ __slots__ = ('column_index',
+ 'width',
+ 'auto_size',
+ 'visible',
+ 'outline_level',
+ 'collapsed',
+ 'style_index',)
+
+ def __init__(self, index = 'A'):
+ self.column_index = index
+ self.width = -1
+ self.auto_size = False
+ self.visible = True
+ self.outline_level = 0
+ self.collapsed = False
+ self.style_index = 0
+
+
+class PageMargins(object):
+ """Information about page margins for view/print layouts."""
+
+ def __init__(self):
+ self.left = self.right = 0.7
+ self.top = self.bottom = 0.75
+ self.header = self.footer = 0.3
+
+
+class SheetProtection(object):
+ """Information about protection of various aspects of a sheet."""
+
+ def __init__(self):
+ self.sheet = False
+ self.objects = False
+ self.scenarios = False
+ self.format_cells = False
+ self.format_columns = False
+ self.format_rows = False
+ self.insert_columns = False
+ self.insert_rows = False
+ self.insert_hyperlinks = False
+ self.delete_columns = False
+ self.delete_rows = False
+ self.select_locked_cells = False
+ self.sort = False
+ self.auto_filter = False
+ self.pivot_tables = False
+ self.select_unlocked_cells = False
+ self._password = ''
+
+ def set_password(self, value = '', already_hashed = False):
+ """Set a password on this sheet."""
+ if not already_hashed:
+ value = hash_password(value)
+ self._password = value
+
+ def _set_raw_password(self, value):
+ """Set a password directly, forcing a hash step."""
+ self.set_password(value, already_hashed = False)
+
+ def _get_raw_password(self):
+ """Return the password value, regardless of hash."""
+ return self._password
+
+ password = property(_get_raw_password, _set_raw_password,
+ 'get/set the password (if already hashed, '
+ 'use set_password() instead)')
+
+
+class Worksheet(object):
+ """Represents a worksheet.
+
+ Do not create worksheets yourself,
+ use :func:`openpyxl.workbook.Workbook.create_sheet` instead
+
+ """
+ BREAK_NONE = 0
+ BREAK_ROW = 1
+ BREAK_COLUMN = 2
+
+ SHEETSTATE_VISIBLE = 'visible'
+ SHEETSTATE_HIDDEN = 'hidden'
+ SHEETSTATE_VERYHIDDEN = 'veryHidden'
+
+ def __init__(self, parent_workbook, title = 'Sheet'):
+ self._parent = parent_workbook
+ self._title = ''
+ if not title:
+ self.title = 'Sheet%d' % (1 + len(self._parent.worksheets))
+ else:
+ self.title = title
+ self.row_dimensions = {}
+ self.column_dimensions = {}
+ self._cells = {}
+ self._styles = {}
+ self._charts = []
+ self.relationships = []
+ self.selected_cell = 'A1'
+ self.active_cell = 'A1'
+ self.sheet_state = self.SHEETSTATE_VISIBLE
+ self.page_setup = PageSetup()
+ self.page_margins = PageMargins()
+ self.header_footer = HeaderFooter()
+ self.sheet_view = SheetView()
+ self.protection = SheetProtection()
+ self.show_gridlines = True
+ self.print_gridlines = False
+ self.show_summary_below = True
+ self.show_summary_right = True
+ self.default_row_dimension = RowDimension()
+ self.default_column_dimension = ColumnDimension()
+ self._auto_filter = None
+ self._freeze_panes = None
+
+ def __repr__(self):
+ return '<Worksheet "%s">' % self.title
+
+ def garbage_collect(self):
+ """Delete cells that are not storing a value."""
+ delete_list = [coordinate for coordinate, cell in \
+ self._cells.items() if (cell.value in ('', None) and \
+ hash(cell.style) == _DEFAULTS_STYLE_HASH)]
+ for coordinate in delete_list:
+ del self._cells[coordinate]
+
+ def get_cell_collection(self):
+ """Return an unordered list of the cells in this worksheet."""
+ return self._cells.values()
+
+ def _set_title(self, value):
+ """Set a sheet title, ensuring it is valid."""
+ bad_title_char_re = re.compile(r'[\\*?:/\[\]]')
+ if bad_title_char_re.search(value):
+ msg = 'Invalid character found in sheet title'
+ raise SheetTitleException(msg)
+
+ # check if sheet_name already exists
+ # do this *before* length check
+ if self._parent.get_sheet_by_name(value):
+ # use name, but append with lowest possible integer
+ i = 1
+ while self._parent.get_sheet_by_name('%s%d' % (value, i)):
+ i += 1
+ value = '%s%d' % (value, i)
+ if len(value) > 31:
+ msg = 'Maximum 31 characters allowed in sheet title'
+ raise SheetTitleException(msg)
+ self._title = value
+
+ def _get_title(self):
+ """Return the title for this sheet."""
+ return self._title
+
+ title = property(_get_title, _set_title, doc =
+ 'Get or set the title of the worksheet. '
+ 'Limited to 31 characters, no special characters.')
+
+ def _set_auto_filter(self, range):
+ # Normalize range to a str or None
+ if not range:
+ range = None
+ elif isinstance(range, str):
+ range = range.upper()
+ else: # Assume a range
+ range = range[0][0].address + ':' + range[-1][-1].address
+ self._auto_filter = range
+
+ def _get_auto_filter(self):
+ return self._auto_filter
+
+ auto_filter = property(_get_auto_filter, _set_auto_filter, doc =
+ 'get or set auto filtering on columns')
+ def _set_freeze_panes(self, topLeftCell):
+ if not topLeftCell:
+ topLeftCell = None
+ elif isinstance(topLeftCell, str):
+ topLeftCell = topLeftCell.upper()
+ else: # Assume a cell
+ topLeftCell = topLeftCell.address
+ if topLeftCell == 'A1':
+ topLeftCell = None
+ self._freeze_panes = topLeftCell
+
+ def _get_freeze_panes(self):
+ return self._freeze_panes
+
+ freeze_panes = property(_get_freeze_panes,_set_freeze_panes, doc =
+ "Get or set frozen panes")
+
+ def cell(self, coordinate = None, row = None, column = None):
+ """Returns a cell object based on the given coordinates.
+
+ Usage: cell(coodinate='A15') **or** cell(row=15, column=1)
+
+ If `coordinates` are not given, then row *and* column must be given.
+
+ Cells are kept in a dictionary which is empty at the worksheet
+ creation. Calling `cell` creates the cell in memory when they
+ are first accessed, to reduce memory usage.
+
+ :param coordinate: coordinates of the cell (e.g. 'B12')
+ :type coordinate: string
+
+ :param row: row index of the cell (e.g. 4)
+ :type row: int
+
+ :param column: column index of the cell (e.g. 3)
+ :type column: int
+
+ :raise: InsufficientCoordinatesException when coordinate or (row and column) are not given
+
+ :rtype: :class:`openpyxl.cell.Cell`
+
+ """
+ if not coordinate:
+ if (row is None or column is None):
+ msg = "You have to provide a value either for " \
+ "'coordinate' or for 'row' *and* 'column'"
+ raise InsufficientCoordinatesException(msg)
+ else:
+ coordinate = '%s%s' % (get_column_letter(column + 1), row + 1)
+ else:
+ coordinate = coordinate.replace('$', '')
+
+ return self._get_cell(coordinate)
+
+ def _get_cell(self, coordinate):
+
+ if not coordinate in self._cells:
+ column, row = coordinate_from_string(coordinate)
+ new_cell = cell.Cell(self, column, row)
+ self._cells[coordinate] = new_cell
+ if column not in self.column_dimensions:
+ self.column_dimensions[column] = ColumnDimension(column)
+ if row not in self.row_dimensions:
+ self.row_dimensions[row] = RowDimension(row)
+ return self._cells[coordinate]
+
+ def get_highest_row(self):
+ """Returns the maximum row index containing data
+
+ :rtype: int
+ """
+ if self.row_dimensions:
+ return max(self.row_dimensions.keys())
+ else:
+ return 1
+
+ def get_highest_column(self):
+ """Get the largest value for column currently stored.
+
+ :rtype: int
+ """
+ if self.column_dimensions:
+ return max([column_index_from_string(column_index)
+ for column_index in self.column_dimensions])
+ else:
+ return 1
+
+ def calculate_dimension(self):
+ """Return the minimum bounding range for all cells containing data."""
+ return 'A1:%s%d' % (get_column_letter(self.get_highest_column()),
+ self.get_highest_row())
+
+ def range(self, range_string, row = 0, column = 0):
+ """Returns a 2D array of cells, with optional row and column offsets.
+
+ :param range_string: cell range string or `named range` name
+ :type range_string: string
+
+ :param row: number of rows to offset
+ :type row: int
+
+ :param column: number of columns to offset
+ :type column: int
+
+ :rtype: tuples of tuples of :class:`openpyxl.cell.Cell`
+
+ """
+ if ':' in range_string:
+ # R1C1 range
+ result = []
+ min_range, max_range = range_string.split(':')
+ min_col, min_row = coordinate_from_string(min_range)
+ max_col, max_row = coordinate_from_string(max_range)
+ if column:
+ min_col = get_column_letter(
+ column_index_from_string(min_col) + column)
+ max_col = get_column_letter(
+ column_index_from_string(max_col) + column)
+ min_col = column_index_from_string(min_col)
+ max_col = column_index_from_string(max_col)
+ cache_cols = {}
+ for col in xrange(min_col, max_col + 1):
+ cache_cols[col] = get_column_letter(col)
+ rows = xrange(min_row + row, max_row + row + 1)
+ cols = xrange(min_col, max_col + 1)
+ for row in rows:
+ new_row = []
+ for col in cols:
+ new_row.append(self.cell('%s%s' % (cache_cols[col], row)))
+ result.append(tuple(new_row))
+ return tuple(result)
+ else:
+ try:
+ return self.cell(coordinate = range_string, row = row,
+ column = column)
+ except CellCoordinatesException:
+ pass
+
+ # named range
+ named_range = self._parent.get_named_range(range_string)
+ if named_range is None:
+ msg = '%s is not a valid range name' % range_string
+ raise NamedRangeException(msg)
+
+ result = []
+ for destination in named_range.destinations:
+
+ worksheet, cells_range = destination
+
+ if worksheet is not self:
+ msg = 'Range %s is not defined on worksheet %s' % \
+ (cells_range, self.title)
+ raise NamedRangeException(msg)
+
+ content = self.range(cells_range)
+
+ if isinstance(content, tuple):
+ for cells in content:
+ result.extend(cells)
+ else:
+ result.append(content)
+
+ if len(result) == 1:
+ return result[0]
+ else:
+ return tuple(result)
+
+ def get_style(self, coordinate):
+ """Return the style object for the specified cell."""
+ if not coordinate in self._styles:
+ self._styles[coordinate] = Style()
+ return self._styles[coordinate]
+
+ def create_relationship(self, rel_type):
+ """Add a relationship for this sheet."""
+ rel = Relationship(rel_type)
+ self.relationships.append(rel)
+ rel_id = self.relationships.index(rel)
+ rel.id = 'rId' + str(rel_id + 1)
+ return self.relationships[rel_id]
+
+ def add_chart(self, chart):
+ """ Add a chart to the sheet """
+
+ chart._sheet = self
+ self._charts.append(chart)
+
+ def append(self, list_or_dict):
+ """Appends a group of values at the bottom of the current sheet.
+
+ * If it's a list: all values are added in order, starting from the first column
+ * If it's a dict: values are assigned to the columns indicated by the keys (numbers or letters)
+
+ :param list_or_dict: list or dict containing values to append
+ :type list_or_dict: list/tuple or dict
+
+ Usage:
+
+ * append(['This is A1', 'This is B1', 'This is C1'])
+ * **or** append({'A' : 'This is A1', 'C' : 'This is C1'})
+ * **or** append({0 : 'This is A1', 2 : 'This is C1'})
+
+ :raise: TypeError when list_or_dict is neither a list/tuple nor a dict
+
+ """
+
+ row_idx = len(self.row_dimensions)
+
+ if isinstance(list_or_dict, (list, tuple)):
+
+ for col_idx, content in enumerate(list_or_dict):
+
+ self.cell(row = row_idx, column = col_idx).value = content
+
+ elif isinstance(list_or_dict, dict):
+
+ for col_idx, content in list_or_dict.items():
+
+ if isinstance(col_idx, basestring):
+ col_idx = column_index_from_string(col_idx) - 1
+
+ self.cell(row = row_idx, column = col_idx).value = content
+
+ else:
+ raise TypeError('list_or_dict must be a list or a dict')
+
+ @property
+ def rows(self):
+
+ return self.range(self.calculate_dimension())
+
+ @property
+ def columns(self):
+
+ max_row = self.get_highest_row()
+
+ cols = []
+
+ for col_idx in range(self.get_highest_column()):
+ col = get_column_letter(col_idx+1)
+ res = self.range('%s1:%s%d' % (col, col, max_row))
+ cols.append(tuple([x[0] for x in res]))
+
+
+ return tuple(cols)
+
diff --git a/tablib/packages/openpyxl/writer/__init__.py b/tablib/packages/openpyxl/writer/__init__.py
new file mode 100644
index 0000000..9eb0a21
--- /dev/null
+++ b/tablib/packages/openpyxl/writer/__init__.py
@@ -0,0 +1,34 @@
+# file openpyxl/writer/__init__.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Imports for the openpyxl.writer namespace."""
+
+# package imports
+from . import excel
+from . import strings
+from . import styles
+from . import theme
+from . import workbook
+from . import worksheet
diff --git a/tablib/packages/openpyxl/writer/charts.py b/tablib/packages/openpyxl/writer/charts.py
new file mode 100644
index 0000000..2c8df39
--- /dev/null
+++ b/tablib/packages/openpyxl/writer/charts.py
@@ -0,0 +1,261 @@
+# coding=UTF-8
+'''
+Copyright (c) 2010 openpyxl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+@license: http://www.opensource.org/licenses/mit-license.php
+@author: Eric Gazoni
+'''
+
+from ..shared.xmltools import Element, SubElement, get_document_content
+from ..chart import Chart, ErrorBar
+
+class ChartWriter(object):
+
+ def __init__(self, chart):
+ self.chart = chart
+
+ def write(self):
+ """ write a chart """
+
+ root = Element('c:chartSpace',
+ {'xmlns:c':"http://schemas.openxmlformats.org/drawingml/2006/chart",
+ 'xmlns:a':"http://schemas.openxmlformats.org/drawingml/2006/main",
+ 'xmlns:r':"http://schemas.openxmlformats.org/officeDocument/2006/relationships"})
+
+ SubElement(root, 'c:lang', {'val':self.chart.lang})
+ self._write_chart(root)
+ self._write_print_settings(root)
+ self._write_shapes(root)
+
+ return get_document_content(root)
+
+ def _write_chart(self, root):
+
+ chart = self.chart
+
+ ch = SubElement(root, 'c:chart')
+ self._write_title(ch)
+ plot_area = SubElement(ch, 'c:plotArea')
+ layout = SubElement(plot_area, 'c:layout')
+ mlayout = SubElement(layout, 'c:manualLayout')
+ SubElement(mlayout, 'c:layoutTarget', {'val':'inner'})
+ SubElement(mlayout, 'c:xMode', {'val':'edge'})
+ SubElement(mlayout, 'c:yMode', {'val':'edge'})
+ SubElement(mlayout, 'c:x', {'val':str(chart._get_margin_left())})
+ SubElement(mlayout, 'c:y', {'val':str(chart._get_margin_top())})
+ SubElement(mlayout, 'c:w', {'val':str(chart.width)})
+ SubElement(mlayout, 'c:h', {'val':str(chart.height)})
+
+ if chart.type == Chart.SCATTER_CHART:
+ subchart = SubElement(plot_area, 'c:scatterChart')
+ SubElement(subchart, 'c:scatterStyle', {'val':str('lineMarker')})
+ else:
+ if chart.type == Chart.BAR_CHART:
+ subchart = SubElement(plot_area, 'c:barChart')
+ SubElement(subchart, 'c:barDir', {'val':'col'})
+ else:
+ subchart = SubElement(plot_area, 'c:lineChart')
+
+ SubElement(subchart, 'c:grouping', {'val':chart.grouping})
+
+ self._write_series(subchart)
+
+ SubElement(subchart, 'c:marker', {'val':'1'})
+ SubElement(subchart, 'c:axId', {'val':str(chart.x_axis.id)})
+ SubElement(subchart, 'c:axId', {'val':str(chart.y_axis.id)})
+
+ if chart.type == Chart.SCATTER_CHART:
+ self._write_axis(plot_area, chart.x_axis, 'c:valAx')
+ else:
+ self._write_axis(plot_area, chart.x_axis, 'c:catAx')
+ self._write_axis(plot_area, chart.y_axis, 'c:valAx')
+
+ self._write_legend(ch)
+
+ SubElement(ch, 'c:plotVisOnly', {'val':'1'})
+
+ def _write_title(self, chart):
+ if self.chart.title != '':
+ title = SubElement(chart, 'c:title')
+ tx = SubElement(title, 'c:tx')
+ rich = SubElement(tx, 'c:rich')
+ SubElement(rich, 'a:bodyPr')
+ SubElement(rich, 'a:lstStyle')
+ p = SubElement(rich, 'a:p')
+ pPr = SubElement(p, 'a:pPr')
+ SubElement(pPr, 'a:defRPr')
+ r = SubElement(p, 'a:r')
+ SubElement(r, 'a:rPr', {'lang':self.chart.lang})
+ t = SubElement(r, 'a:t').text = self.chart.title
+ SubElement(title, 'c:layout')
+
+ def _write_axis(self, plot_area, axis, label):
+
+ ax = SubElement(plot_area, label)
+ SubElement(ax, 'c:axId', {'val':str(axis.id)})
+
+ scaling = SubElement(ax, 'c:scaling')
+ SubElement(scaling, 'c:orientation', {'val':axis.orientation})
+ if label == 'c:valAx':
+ SubElement(scaling, 'c:max', {'val':str(axis.max)})
+ SubElement(scaling, 'c:min', {'val':str(axis.min)})
+
+ SubElement(ax, 'c:axPos', {'val':axis.position})
+ if label == 'c:valAx':
+ SubElement(ax, 'c:majorGridlines')
+ SubElement(ax, 'c:numFmt', {'formatCode':"General", 'sourceLinked':'1'})
+ SubElement(ax, 'c:tickLblPos', {'val':axis.tick_label_position})
+ SubElement(ax, 'c:crossAx', {'val':str(axis.cross)})
+ SubElement(ax, 'c:crosses', {'val':axis.crosses})
+ if axis.auto:
+ SubElement(ax, 'c:auto', {'val':'1'})
+ if axis.label_align:
+ SubElement(ax, 'c:lblAlgn', {'val':axis.label_align})
+ if axis.label_offset:
+ SubElement(ax, 'c:lblOffset', {'val':str(axis.label_offset)})
+ if label == 'c:valAx':
+ if self.chart.type == Chart.SCATTER_CHART:
+ SubElement(ax, 'c:crossBetween', {'val':'midCat'})
+ else:
+ SubElement(ax, 'c:crossBetween', {'val':'between'})
+ SubElement(ax, 'c:majorUnit', {'val':str(axis.unit)})
+
+ def _write_series(self, subchart):
+
+ for i, serie in enumerate(self.chart._series):
+ ser = SubElement(subchart, 'c:ser')
+ SubElement(ser, 'c:idx', {'val':str(i)})
+ SubElement(ser, 'c:order', {'val':str(i)})
+
+ if serie.legend:
+ tx = SubElement(ser, 'c:tx')
+ self._write_serial(tx, serie.legend)
+
+ if serie.color:
+ sppr = SubElement(ser, 'c:spPr')
+ if self.chart.type == Chart.BAR_CHART:
+ # fill color
+ fillc = SubElement(sppr, 'a:solidFill')
+ SubElement(fillc, 'a:srgbClr', {'val':serie.color})
+ # edge color
+ ln = SubElement(sppr, 'a:ln')
+ fill = SubElement(ln, 'a:solidFill')
+ SubElement(fill, 'a:srgbClr', {'val':serie.color})
+
+ if serie.error_bar:
+ self._write_error_bar(ser, serie)
+
+ marker = SubElement(ser, 'c:marker')
+ SubElement(marker, 'c:symbol', {'val':serie.marker})
+
+ if serie.labels:
+ cat = SubElement(ser, 'c:cat')
+ self._write_serial(cat, serie.labels)
+
+ if self.chart.type == Chart.SCATTER_CHART:
+ if serie.xvalues:
+ xval = SubElement(ser, 'c:xVal')
+ self._write_serial(xval, serie.xvalues)
+
+ yval = SubElement(ser, 'c:yVal')
+ self._write_serial(yval, serie.values)
+ else:
+ val = SubElement(ser, 'c:val')
+ self._write_serial(val, serie.values)
+
+ def _write_serial(self, node, serie, literal=False):
+
+ cache = serie._get_cache()
+ if isinstance(cache[0], basestring):
+ typ = 'str'
+ else:
+ typ = 'num'
+
+ if not literal:
+ if typ == 'num':
+ ref = SubElement(node, 'c:numRef')
+ else:
+ ref = SubElement(node, 'c:strRef')
+ SubElement(ref, 'c:f').text = serie._get_ref()
+ if typ == 'num':
+ data = SubElement(ref, 'c:numCache')
+ else:
+ data = SubElement(ref, 'c:strCache')
+ else:
+ data = SubElement(node, 'c:numLit')
+
+ if typ == 'num':
+ SubElement(data, 'c:formatCode').text = 'General'
+ if literal:
+ values = (1,)
+ else:
+ values = cache
+
+ SubElement(data, 'c:ptCount', {'val':str(len(values))})
+ for j, val in enumerate(values):
+ point = SubElement(data, 'c:pt', {'idx':str(j)})
+ SubElement(point, 'c:v').text = str(val)
+
+ def _write_error_bar(self, node, serie):
+
+ flag = {ErrorBar.PLUS_MINUS:'both',
+ ErrorBar.PLUS:'plus',
+ ErrorBar.MINUS:'minus'}
+
+ eb = SubElement(node, 'c:errBars')
+ SubElement(eb, 'c:errBarType', {'val':flag[serie.error_bar.type]})
+ SubElement(eb, 'c:errValType', {'val':'cust'})
+
+ plus = SubElement(eb, 'c:plus')
+ self._write_serial(plus, serie.error_bar.values,
+ literal=(serie.error_bar.type==ErrorBar.MINUS))
+
+ minus = SubElement(eb, 'c:minus')
+ self._write_serial(minus, serie.error_bar.values,
+ literal=(serie.error_bar.type==ErrorBar.PLUS))
+
+ def _write_legend(self, chart):
+
+ legend = SubElement(chart, 'c:legend')
+ SubElement(legend, 'c:legendPos', {'val':self.chart.legend.position})
+ SubElement(legend, 'c:layout')
+
+ def _write_print_settings(self, root):
+
+ settings = SubElement(root, 'c:printSettings')
+ SubElement(settings, 'c:headerFooter')
+ margins = dict([(k, str(v)) for (k,v) in self.chart.print_margins.items()])
+ SubElement(settings, 'c:pageMargins', margins)
+ SubElement(settings, 'c:pageSetup')
+
+ def _write_shapes(self, root):
+
+ if self.chart._shapes:
+ SubElement(root, 'c:userShapes', {'r:id':'rId1'})
+
+ def write_rels(self, drawing_id):
+
+ root = Element('Relationships', {'xmlns' : 'http://schemas.openxmlformats.org/package/2006/relationships'})
+ attrs = {'Id' : 'rId1',
+ 'Type' : 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUserShapes',
+ 'Target' : '../drawings/drawing%s.xml' % drawing_id }
+ SubElement(root, 'Relationship', attrs)
+ return get_document_content(root)
diff --git a/tablib/packages/openpyxl/writer/drawings.py b/tablib/packages/openpyxl/writer/drawings.py
new file mode 100644
index 0000000..8a6cce2
--- /dev/null
+++ b/tablib/packages/openpyxl/writer/drawings.py
@@ -0,0 +1,192 @@
+# coding=UTF-8
+'''
+Copyright (c) 2010 openpyxl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+@license: http://www.opensource.org/licenses/mit-license.php
+@author: Eric Gazoni
+'''
+
+from ..shared.xmltools import Element, SubElement, get_document_content
+
+
+class DrawingWriter(object):
+ """ one main drawing file per sheet """
+
+ def __init__(self, sheet):
+ self._sheet = sheet
+
+ def write(self):
+ """ write drawings for one sheet in one file """
+
+ root = Element('xdr:wsDr',
+ {'xmlns:xdr' : "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing",
+ 'xmlns:a' : "http://schemas.openxmlformats.org/drawingml/2006/main"})
+
+ for i, chart in enumerate(self._sheet._charts):
+
+ drawing = chart.drawing
+
+# anchor = SubElement(root, 'xdr:twoCellAnchor')
+# (start_row, start_col), (end_row, end_col) = drawing.coordinates
+# # anchor coordinates
+# _from = SubElement(anchor, 'xdr:from')
+# x = SubElement(_from, 'xdr:col').text = str(start_col)
+# x = SubElement(_from, 'xdr:colOff').text = '0'
+# x = SubElement(_from, 'xdr:row').text = str(start_row)
+# x = SubElement(_from, 'xdr:rowOff').text = '0'
+
+# _to = SubElement(anchor, 'xdr:to')
+# x = SubElement(_to, 'xdr:col').text = str(end_col)
+# x = SubElement(_to, 'xdr:colOff').text = '0'
+# x = SubElement(_to, 'xdr:row').text = str(end_row)
+# x = SubElement(_to, 'xdr:rowOff').text = '0'
+
+ # we only support absolute anchor atm (TODO: oneCellAnchor, twoCellAnchor
+ x, y, w, h = drawing.get_emu_dimensions()
+ anchor = SubElement(root, 'xdr:absoluteAnchor')
+ SubElement(anchor, 'xdr:pos', {'x':str(x), 'y':str(y)})
+ SubElement(anchor, 'xdr:ext', {'cx':str(w), 'cy':str(h)})
+
+ # graph frame
+ frame = SubElement(anchor, 'xdr:graphicFrame', {'macro':''})
+
+ name = SubElement(frame, 'xdr:nvGraphicFramePr')
+ SubElement(name, 'xdr:cNvPr', {'id':'%s' % i, 'name':'Graphique %s' % i})
+ SubElement(name, 'xdr:cNvGraphicFramePr')
+
+ frm = SubElement(frame, 'xdr:xfrm')
+ # no transformation
+ SubElement(frm, 'a:off', {'x':'0', 'y':'0'})
+ SubElement(frm, 'a:ext', {'cx':'0', 'cy':'0'})
+
+ graph = SubElement(frame, 'a:graphic')
+ data = SubElement(graph, 'a:graphicData',
+ {'uri':'http://schemas.openxmlformats.org/drawingml/2006/chart'})
+ SubElement(data, 'c:chart',
+ { 'xmlns:c':'http://schemas.openxmlformats.org/drawingml/2006/chart',
+ 'xmlns:r':'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
+ 'r:id':'rId%s' % (i + 1)})
+
+ SubElement(anchor, 'xdr:clientData')
+
+ return get_document_content(root)
+
+ def write_rels(self, chart_id):
+
+ root = Element('Relationships',
+ {'xmlns' : 'http://schemas.openxmlformats.org/package/2006/relationships'})
+ for i, chart in enumerate(self._sheet._charts):
+ attrs = {'Id' : 'rId%s' % (i + 1),
+ 'Type' : 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart',
+ 'Target' : '../charts/chart%s.xml' % (chart_id + i) }
+ SubElement(root, 'Relationship', attrs)
+ return get_document_content(root)
+
+class ShapeWriter(object):
+ """ one file per shape """
+
+ schema = "http://schemas.openxmlformats.org/drawingml/2006/main"
+
+ def __init__(self, shapes):
+
+ self._shapes = shapes
+
+ def write(self, shape_id):
+
+ root = Element('c:userShapes', {'xmlns:c' : 'http://schemas.openxmlformats.org/drawingml/2006/chart'})
+
+ for shape in self._shapes:
+ anchor = SubElement(root, 'cdr:relSizeAnchor',
+ {'xmlns:cdr' : "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"})
+
+ xstart, ystart, xend, yend = shape.get_coordinates()
+
+ _from = SubElement(anchor, 'cdr:from')
+ SubElement(_from, 'cdr:x').text = str(xstart)
+ SubElement(_from, 'cdr:y').text = str(ystart)
+
+ _to = SubElement(anchor, 'cdr:to')
+ SubElement(_to, 'cdr:x').text = str(xend)
+ SubElement(_to, 'cdr:y').text = str(yend)
+
+ sp = SubElement(anchor, 'cdr:sp', {'macro':'', 'textlink':''})
+ nvspr = SubElement(sp, 'cdr:nvSpPr')
+ SubElement(nvspr, 'cdr:cNvPr', {'id':str(shape_id), 'name':'shape %s' % shape_id})
+ SubElement(nvspr, 'cdr:cNvSpPr')
+
+ sppr = SubElement(sp, 'cdr:spPr')
+ frm = SubElement(sppr, 'a:xfrm', {'xmlns:a':self.schema})
+ # no transformation
+ SubElement(frm, 'a:off', {'x':'0', 'y':'0'})
+ SubElement(frm, 'a:ext', {'cx':'0', 'cy':'0'})
+
+ prstgeom = SubElement(sppr, 'a:prstGeom', {'xmlns:a':self.schema, 'prst':str(shape.style)})
+ SubElement(prstgeom, 'a:avLst')
+
+ fill = SubElement(sppr, 'a:solidFill', {'xmlns:a':self.schema})
+ SubElement(fill, 'a:srgbClr', {'val':shape.color})
+
+ border = SubElement(sppr, 'a:ln', {'xmlns:a':self.schema, 'w':str(shape._border_width)})
+ sf = SubElement(border, 'a:solidFill')
+ SubElement(sf, 'a:srgbClr', {'val':shape.border_color})
+
+ self._write_style(sp)
+ self._write_text(sp, shape)
+
+ shape_id += 1
+
+ return get_document_content(root)
+
+ def _write_text(self, node, shape):
+ """ write text in the shape """
+
+ tx_body = SubElement(node, 'cdr:txBody')
+ SubElement(tx_body, 'a:bodyPr', {'xmlns:a':self.schema, 'vertOverflow':'clip'})
+ SubElement(tx_body, 'a:lstStyle',
+ {'xmlns:a':self.schema})
+ p = SubElement(tx_body, 'a:p', {'xmlns:a':self.schema})
+ if shape.text:
+ r = SubElement(p, 'a:r')
+ rpr = SubElement(r, 'a:rPr', {'lang':'en-US'})
+ fill = SubElement(rpr, 'a:solidFill')
+ SubElement(fill, 'a:srgbClr', {'val':shape.text_color})
+
+ SubElement(r, 'a:t').text = shape.text
+ else:
+ SubElement(p, 'a:endParaRPr', {'lang':'en-US'})
+
+ def _write_style(self, node):
+ """ write style theme """
+
+ style = SubElement(node, 'cdr:style')
+
+ ln_ref = SubElement(style, 'a:lnRef', {'xmlns:a':self.schema, 'idx':'2'})
+ scheme_clr = SubElement(ln_ref, 'a:schemeClr', {'val':'accent1'})
+ SubElement(scheme_clr, 'a:shade', {'val':'50000'})
+
+ fill_ref = SubElement(style, 'a:fillRef', {'xmlns:a':self.schema, 'idx':'1'})
+ SubElement(fill_ref, 'a:schemeClr', {'val':'accent1'})
+
+ effect_ref = SubElement(style, 'a:effectRef', {'xmlns:a':self.schema, 'idx':'0'})
+ SubElement(effect_ref, 'a:schemeClr', {'val':'accent1'})
+
+ font_ref = SubElement(style, 'a:fontRef', {'xmlns:a':self.schema, 'idx':'minor'})
+ SubElement(font_ref, 'a:schemeClr', {'val':'lt1'})
diff --git a/tablib/packages/openpyxl/writer/dump_worksheet.py b/tablib/packages/openpyxl/writer/dump_worksheet.py
new file mode 100644
index 0000000..7f098f5
--- /dev/null
+++ b/tablib/packages/openpyxl/writer/dump_worksheet.py
@@ -0,0 +1,256 @@
+# file openpyxl/writer/straight_worksheet.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write worksheets to xml representations in an optimized way"""
+
+import datetime
+import os
+
+from ..cell import column_index_from_string, get_column_letter, Cell
+from ..worksheet import Worksheet
+from ..shared.xmltools import XMLGenerator, get_document_content, \
+ start_tag, end_tag, tag
+from ..shared.date_time import SharedDate
+from ..shared.ooxml import MAX_COLUMN, MAX_ROW
+from tempfile import NamedTemporaryFile
+from ..writer.excel import ExcelWriter
+from ..writer.strings import write_string_table
+from ..writer.styles import StyleWriter
+from ..style import Style, NumberFormat
+
+from ..shared.ooxml import ARC_SHARED_STRINGS, ARC_CONTENT_TYPES, \
+ ARC_ROOT_RELS, ARC_WORKBOOK_RELS, ARC_APP, ARC_CORE, ARC_THEME, \
+ ARC_STYLE, ARC_WORKBOOK, \
+ PACKAGE_WORKSHEETS, PACKAGE_DRAWINGS, PACKAGE_CHARTS
+
+STYLES = {'datetime' : {'type':Cell.TYPE_NUMERIC,
+ 'style':'1'},
+ 'string':{'type':Cell.TYPE_STRING,
+ 'style':'0'},
+ 'numeric':{'type':Cell.TYPE_NUMERIC,
+ 'style':'0'},
+ 'formula':{'type':Cell.TYPE_FORMULA,
+ 'style':'0'},
+ 'boolean':{'type':Cell.TYPE_BOOL,
+ 'style':'0'},
+ }
+
+DATETIME_STYLE = Style()
+DATETIME_STYLE.number_format.format_code = NumberFormat.FORMAT_DATE_YYYYMMDD2
+BOUNDING_BOX_PLACEHOLDER = 'A1:%s%d' % (get_column_letter(MAX_COLUMN), MAX_ROW)
+
+class DumpWorksheet(Worksheet):
+
+ """
+ .. warning::
+
+ You shouldn't initialize this yourself, use :class:`openpyxl.workbook.Workbook` constructor instead,
+ with `optimized_write = True`.
+ """
+
+ def __init__(self, parent_workbook):
+
+ Worksheet.__init__(self, parent_workbook)
+
+ self._max_col = 0
+ self._max_row = 0
+ self._parent = parent_workbook
+ self._fileobj_header = NamedTemporaryFile(mode='r+', prefix='openpyxl.', suffix='.header', delete=False)
+ self._fileobj_content = NamedTemporaryFile(mode='r+', prefix='openpyxl.', suffix='.content', delete=False)
+ self._fileobj = NamedTemporaryFile(mode='w', prefix='openpyxl.', delete=False)
+ self.doc = XMLGenerator(self._fileobj_content, 'utf-8')
+ self.header = XMLGenerator(self._fileobj_header, 'utf-8')
+ self.title = 'Sheet'
+
+ self._shared_date = SharedDate()
+ self._string_builder = self._parent.strings_table_builder
+
+ @property
+ def filename(self):
+ return self._fileobj.name
+
+ def write_header(self):
+
+ doc = self.header
+
+ start_tag(doc, 'worksheet',
+ {'xml:space': 'preserve',
+ 'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'})
+ start_tag(doc, 'sheetPr')
+ tag(doc, 'outlinePr',
+ {'summaryBelow': '1',
+ 'summaryRight': '1'})
+ end_tag(doc, 'sheetPr')
+ tag(doc, 'dimension', {'ref': 'A1:%s' % (self.get_dimensions())})
+ start_tag(doc, 'sheetViews')
+ start_tag(doc, 'sheetView', {'workbookViewId': '0'})
+ tag(doc, 'selection', {'activeCell': 'A1',
+ 'sqref': 'A1'})
+ end_tag(doc, 'sheetView')
+ end_tag(doc, 'sheetViews')
+ tag(doc, 'sheetFormatPr', {'defaultRowHeight': '15'})
+ start_tag(doc, 'sheetData')
+
+ def close(self):
+
+ self._close_content()
+ self._close_header()
+
+ self._write_fileobj(self._fileobj_header)
+ self._write_fileobj(self._fileobj_content)
+
+ self._fileobj.close()
+
+ def _write_fileobj(self, fobj):
+
+ fobj.flush()
+ fobj.seek(0)
+
+ while True:
+ chunk = fobj.read(4096)
+ if not chunk:
+ break
+ self._fileobj.write(chunk)
+
+ fobj.close()
+ os.remove(fobj.name)
+
+ self._fileobj.flush()
+
+ def _close_header(self):
+
+ doc = self.header
+ #doc.endDocument()
+
+ def _close_content(self):
+
+ doc = self.doc
+ end_tag(doc, 'sheetData')
+
+ end_tag(doc, 'worksheet')
+ #doc.endDocument()
+
+ def get_dimensions(self):
+
+ if not self._max_col or not self._max_row:
+ return 'A1'
+ else:
+ return '%s%d' % (get_column_letter(self._max_col), (self._max_row))
+
+ def append(self, row):
+
+ """
+ :param row: iterable containing values to append
+ :type row: iterable
+ """
+
+ doc = self.doc
+
+ self._max_row += 1
+ span = len(row)
+ self._max_col = max(self._max_col, span)
+
+ row_idx = self._max_row
+
+ attrs = {'r': '%d' % row_idx,
+ 'spans': '1:%d' % span}
+
+ start_tag(doc, 'row', attrs)
+
+ for col_idx, cell in enumerate(row):
+
+ if cell is None:
+ continue
+
+ coordinate = '%s%d' % (get_column_letter(col_idx+1), row_idx)
+ attributes = {'r': coordinate}
+
+ if isinstance(cell, bool):
+ dtype = 'boolean'
+ elif isinstance(cell, (int, float)):
+ dtype = 'numeric'
+ elif isinstance(cell, (datetime.datetime, datetime.date)):
+ dtype = 'datetime'
+ cell = self._shared_date.datetime_to_julian(cell)
+ attributes['s'] = STYLES[dtype]['style']
+ elif cell and cell[0] == '=':
+ dtype = 'formula'
+ else:
+ dtype = 'string'
+ cell = self._string_builder.add(cell)
+
+ attributes['t'] = STYLES[dtype]['type']
+
+ start_tag(doc, 'c', attributes)
+
+ if dtype == 'formula':
+ tag(doc, 'f', body = '%s' % cell[1:])
+ tag(doc, 'v')
+ else:
+ tag(doc, 'v', body = '%s' % cell)
+
+ end_tag(doc, 'c')
+
+
+ end_tag(doc, 'row')
+
+
+def save_dump(workbook, filename):
+
+ writer = ExcelDumpWriter(workbook)
+ writer.save(filename)
+ return True
+
+class ExcelDumpWriter(ExcelWriter):
+
+ def __init__(self, workbook):
+
+ self.workbook = workbook
+ self.style_writer = StyleDumpWriter(workbook)
+ self.style_writer._style_list.append(DATETIME_STYLE)
+
+ def _write_string_table(self, archive):
+
+ shared_string_table = self.workbook.strings_table_builder.get_table()
+ archive.writestr(ARC_SHARED_STRINGS,
+ write_string_table(shared_string_table))
+
+ return shared_string_table
+
+ def _write_worksheets(self, archive, shared_string_table, style_writer):
+
+ for i, sheet in enumerate(self.workbook.worksheets):
+ sheet.write_header()
+ sheet.close()
+ archive.write(sheet.filename, PACKAGE_WORKSHEETS + '/sheet%d.xml' % (i + 1))
+ os.remove(sheet.filename)
+
+
+class StyleDumpWriter(StyleWriter):
+
+ def _get_style_list(self, workbook):
+ return []
+
diff --git a/tablib/packages/openpyxl/writer/excel.py b/tablib/packages/openpyxl/writer/excel.py
new file mode 100644
index 0000000..b95245e
--- /dev/null
+++ b/tablib/packages/openpyxl/writer/excel.py
@@ -0,0 +1,161 @@
+# file openpyxl/writer/excel.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write a .xlsx file."""
+
+# Python stdlib imports
+from zipfile import ZipFile, ZIP_DEFLATED
+from ....compat import BytesIO as StringIO
+
+# package imports
+from ..shared.ooxml import ARC_SHARED_STRINGS, ARC_CONTENT_TYPES, \
+ ARC_ROOT_RELS, ARC_WORKBOOK_RELS, ARC_APP, ARC_CORE, ARC_THEME, \
+ ARC_STYLE, ARC_WORKBOOK, \
+ PACKAGE_WORKSHEETS, PACKAGE_DRAWINGS, PACKAGE_CHARTS
+from ..writer.strings import create_string_table, write_string_table
+from ..writer.workbook import write_content_types, write_root_rels, \
+ write_workbook_rels, write_properties_app, write_properties_core, \
+ write_workbook
+from ..writer.theme import write_theme
+from ..writer.styles import StyleWriter
+from ..writer.drawings import DrawingWriter, ShapeWriter
+from ..writer.charts import ChartWriter
+from ..writer.worksheet import write_worksheet, write_worksheet_rels
+
+
+class ExcelWriter(object):
+ """Write a workbook object to an Excel file."""
+
+ def __init__(self, workbook):
+ self.workbook = workbook
+ self.style_writer = StyleWriter(self.workbook)
+
+ def write_data(self, archive):
+ """Write the various xml files into the zip archive."""
+ # cleanup all worksheets
+ shared_string_table = self._write_string_table(archive)
+
+ archive.writestr(ARC_CONTENT_TYPES, write_content_types(self.workbook))
+ archive.writestr(ARC_ROOT_RELS, write_root_rels(self.workbook))
+ archive.writestr(ARC_WORKBOOK_RELS, write_workbook_rels(self.workbook))
+ archive.writestr(ARC_APP, write_properties_app(self.workbook))
+ archive.writestr(ARC_CORE,
+ write_properties_core(self.workbook.properties))
+ archive.writestr(ARC_THEME, write_theme())
+ archive.writestr(ARC_STYLE, self.style_writer.write_table())
+ archive.writestr(ARC_WORKBOOK, write_workbook(self.workbook))
+
+ self._write_worksheets(archive, shared_string_table, self.style_writer)
+
+ def _write_string_table(self, archive):
+
+ for ws in self.workbook.worksheets:
+ ws.garbage_collect()
+ shared_string_table = create_string_table(self.workbook)
+
+
+ archive.writestr(ARC_SHARED_STRINGS,
+ write_string_table(shared_string_table))
+
+ for k, v in shared_string_table.items():
+ shared_string_table[k] = bytes(v)
+
+ return shared_string_table
+
+ def _write_worksheets(self, archive, shared_string_table, style_writer):
+
+ drawing_id = 1
+ chart_id = 1
+ shape_id = 1
+
+ for i, sheet in enumerate(self.workbook.worksheets):
+ archive.writestr(PACKAGE_WORKSHEETS + '/sheet%d.xml' % (i + 1),
+ write_worksheet(sheet, shared_string_table,
+ style_writer.get_style_by_hash()))
+ if sheet._charts or sheet.relationships:
+ archive.writestr(PACKAGE_WORKSHEETS +
+ '/_rels/sheet%d.xml.rels' % (i + 1),
+ write_worksheet_rels(sheet, drawing_id))
+ if sheet._charts:
+ dw = DrawingWriter(sheet)
+ archive.writestr(PACKAGE_DRAWINGS + '/drawing%d.xml' % drawing_id,
+ dw.write())
+ archive.writestr(PACKAGE_DRAWINGS + '/_rels/drawing%d.xml.rels' % drawing_id,
+ dw.write_rels(chart_id))
+ drawing_id += 1
+
+ for chart in sheet._charts:
+ cw = ChartWriter(chart)
+ archive.writestr(PACKAGE_CHARTS + '/chart%d.xml' % chart_id,
+ cw.write())
+
+ if chart._shapes:
+ archive.writestr(PACKAGE_CHARTS + '/_rels/chart%d.xml.rels' % chart_id,
+ cw.write_rels(drawing_id))
+ sw = ShapeWriter(chart._shapes)
+ archive.writestr(PACKAGE_DRAWINGS + '/drawing%d.xml' % drawing_id,
+ sw.write(shape_id))
+ shape_id += len(chart._shapes)
+ drawing_id += 1
+
+ chart_id += 1
+
+
+ def save(self, filename):
+ """Write data into the archive."""
+ archive = ZipFile(filename, 'w', ZIP_DEFLATED)
+ self.write_data(archive)
+ archive.close()
+
+
+def save_workbook(workbook, filename):
+ """Save the given workbook on the filesystem under the name filename.
+
+ :param workbook: the workbook to save
+ :type workbook: :class:`openpyxl.workbook.Workbook`
+
+ :param filename: the path to which save the workbook
+ :type filename: string
+
+ :rtype: bool
+
+ """
+ writer = ExcelWriter(workbook)
+ writer.save(filename)
+ return True
+
+
+def save_virtual_workbook(workbook):
+ """Return an in-memory workbook, suitable for a Django response."""
+ writer = ExcelWriter(workbook)
+ temp_buffer = StringIO()
+ try:
+ archive = ZipFile(temp_buffer, 'w', ZIP_DEFLATED)
+ writer.write_data(archive)
+ finally:
+ archive.close()
+ virtual_workbook = temp_buffer.getvalue()
+ temp_buffer.close()
+ return virtual_workbook
diff --git a/tablib/packages/openpyxl/writer/strings.py b/tablib/packages/openpyxl/writer/strings.py
new file mode 100644
index 0000000..f73daed
--- /dev/null
+++ b/tablib/packages/openpyxl/writer/strings.py
@@ -0,0 +1,86 @@
+# file openpyxl/writer/strings.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write the shared string table."""
+
+# Python stdlib imports
+from ....compat import BytesIO as StringIO
+
+# package imports
+from ..shared.xmltools import start_tag, end_tag, tag, XMLGenerator
+
+
+def create_string_table(workbook):
+ """Compile the string table for a workbook."""
+ strings = set()
+ for sheet in workbook.worksheets:
+ for cell in sheet.get_cell_collection():
+ if cell.data_type == cell.TYPE_STRING and cell._value is not None:
+ strings.add(cell.value)
+ return dict((key, i) for i, key in enumerate(strings))
+
+
+def write_string_table(string_table):
+ """Write the string table xml."""
+ temp_buffer = StringIO()
+ doc = XMLGenerator(temp_buffer, 'utf-8')
+ start_tag(doc, 'sst', {'xmlns':
+ 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
+ 'uniqueCount': '%d' % len(string_table)})
+ strings_to_write = sorted(string_table.items(),
+ key=lambda pair: pair[1])
+ for key in [pair[0] for pair in strings_to_write]:
+ start_tag(doc, 'si')
+ if key.strip() != key:
+ attr = {'xml:space': 'preserve'}
+ else:
+ attr = {}
+ tag(doc, 't', attr, key)
+ end_tag(doc, 'si')
+ end_tag(doc, 'sst')
+ string_table_xml = temp_buffer.getvalue()
+ temp_buffer.close()
+ return string_table_xml
+
+class StringTableBuilder(object):
+
+ def __init__(self):
+
+ self.counter = 0
+ self.dct = {}
+
+ def add(self, key):
+
+ key = key.strip()
+ try:
+ return self.dct[key]
+ except KeyError:
+ res = self.dct[key] = self.counter
+ self.counter += 1
+ return res
+
+ def get_table(self):
+
+ return self.dct
diff --git a/tablib/packages/openpyxl/writer/styles.py b/tablib/packages/openpyxl/writer/styles.py
new file mode 100644
index 0000000..70dd719
--- /dev/null
+++ b/tablib/packages/openpyxl/writer/styles.py
@@ -0,0 +1,256 @@
+# file openpyxl/writer/styles.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write the shared style table."""
+
+# package imports
+from ..shared.xmltools import Element, SubElement
+from ..shared.xmltools import get_document_content
+from .. import style
+
+class StyleWriter(object):
+
+ def __init__(self, workbook):
+ self._style_list = self._get_style_list(workbook)
+ self._root = Element('styleSheet',
+ {'xmlns':'http://schemas.openxmlformats.org/spreadsheetml/2006/main'})
+
+ def _get_style_list(self, workbook):
+ crc = {}
+ for worksheet in workbook.worksheets:
+ for style in worksheet._styles.values():
+ crc[hash(style)] = style
+ self.style_table = dict([(style, i+1) \
+ for i, style in enumerate(crc.values())])
+ sorted_styles = sorted(self.style_table.items(), \
+ key = lambda pair:pair[1])
+ return [s[0] for s in sorted_styles]
+
+ def get_style_by_hash(self):
+ return dict([(hash(style), id) \
+ for style, id in self.style_table.items()])
+
+ def write_table(self):
+ number_format_table = self._write_number_formats()
+ fonts_table = self._write_fonts()
+ fills_table = self._write_fills()
+ borders_table = self._write_borders()
+ self._write_cell_style_xfs()
+ self._write_cell_xfs(number_format_table, fonts_table, fills_table, borders_table)
+ self._write_cell_style()
+ self._write_dxfs()
+ self._write_table_styles()
+
+ return get_document_content(xml_node=self._root)
+
+ def _write_fonts(self):
+ """ add fonts part to root
+ return {font.crc => index}
+ """
+
+ fonts = SubElement(self._root, 'fonts')
+
+ # default
+ font_node = SubElement(fonts, 'font')
+ SubElement(font_node, 'sz', {'val':'11'})
+ SubElement(font_node, 'color', {'theme':'1'})
+ SubElement(font_node, 'name', {'val':'Calibri'})
+ SubElement(font_node, 'family', {'val':'2'})
+ SubElement(font_node, 'scheme', {'val':'minor'})
+
+ # others
+ table = {}
+ index = 1
+ for st in self._style_list:
+ if hash(st.font) != hash(style.DEFAULTS.font) and hash(st.font) not in table:
+ table[hash(st.font)] = str(index)
+ font_node = SubElement(fonts, 'font')
+ SubElement(font_node, 'sz', {'val':str(st.font.size)})
+ SubElement(font_node, 'color', {'rgb':str(st.font.color.index)})
+ SubElement(font_node, 'name', {'val':st.font.name})
+ SubElement(font_node, 'family', {'val':'2'})
+ SubElement(font_node, 'scheme', {'val':'minor'})
+ if st.font.bold:
+ SubElement(font_node, 'b')
+ if st.font.italic:
+ SubElement(font_node, 'i')
+ index += 1
+
+ fonts.attrib["count"] = str(index)
+ return table
+
+ def _write_fills(self):
+ fills = SubElement(self._root, 'fills', {'count':'2'})
+ fill = SubElement(fills, 'fill')
+ SubElement(fill, 'patternFill', {'patternType':'none'})
+ fill = SubElement(fills, 'fill')
+ SubElement(fill, 'patternFill', {'patternType':'gray125'})
+
+ table = {}
+ index = 2
+ for st in self._style_list:
+ if hash(st.fill) != hash(style.DEFAULTS.fill) and hash(st.fill) not in table:
+ table[hash(st.fill)] = str(index)
+ fill = SubElement(fills, 'fill')
+ if hash(st.fill.fill_type) != hash(style.DEFAULTS.fill.fill_type):
+ node = SubElement(fill,'patternFill', {'patternType':st.fill.fill_type})
+ if hash(st.fill.start_color) != hash(style.DEFAULTS.fill.start_color):
+
+ SubElement(node, 'fgColor', {'rgb':str(st.fill.start_color.index)})
+ if hash(st.fill.end_color) != hash(style.DEFAULTS.fill.end_color):
+ SubElement(node, 'bgColor', {'rgb':str(st.fill.start_color.index)})
+ index += 1
+
+ fills.attrib["count"] = str(index)
+ return table
+
+ def _write_borders(self):
+ borders = SubElement(self._root, 'borders')
+
+ # default
+ border = SubElement(borders, 'border')
+ SubElement(border, 'left')
+ SubElement(border, 'right')
+ SubElement(border, 'top')
+ SubElement(border, 'bottom')
+ SubElement(border, 'diagonal')
+
+ # others
+ table = {}
+ index = 1
+ for st in self._style_list:
+ if hash(st.borders) != hash(style.DEFAULTS.borders) and hash(st.borders) not in table:
+ table[hash(st.borders)] = str(index)
+ border = SubElement(borders, 'border')
+ # caution: respect this order
+ for side in ('left','right','top','bottom','diagonal'):
+ obj = getattr(st.borders, side)
+ node = SubElement(border, side, {'style':obj.border_style})
+ SubElement(node, 'color', {'rgb':str(obj.color.index)})
+ index += 1
+
+ borders.attrib["count"] = str(index)
+ return table
+
+ def _write_cell_style_xfs(self):
+ cell_style_xfs = SubElement(self._root, 'cellStyleXfs', {'count':'1'})
+ xf = SubElement(cell_style_xfs, 'xf',
+ {'numFmtId':"0", 'fontId':"0", 'fillId':"0", 'borderId':"0"})
+
+ def _write_cell_xfs(self, number_format_table, fonts_table, fills_table, borders_table):
+ """ write styles combinations based on ids found in tables """
+
+ # writing the cellXfs
+ cell_xfs = SubElement(self._root, 'cellXfs',
+ {'count':'%d' % (len(self._style_list) + 1)})
+
+ # default
+ def _get_default_vals():
+ return dict(numFmtId='0', fontId='0', fillId='0',
+ xfId='0', borderId='0')
+
+ SubElement(cell_xfs, 'xf', _get_default_vals())
+
+ for st in self._style_list:
+ vals = _get_default_vals()
+
+ if hash(st.font) != hash(style.DEFAULTS.font):
+ vals['fontId'] = fonts_table[hash(st.font)]
+ vals['applyFont'] = '1'
+
+ if hash(st.borders) != hash(style.DEFAULTS.borders):
+ vals['borderId'] = borders_table[hash(st.borders)]
+ vals['applyBorder'] = '1'
+
+ if hash(st.fill) != hash(style.DEFAULTS.fill):
+ vals['fillId'] = fills_table[hash(st.fill)]
+ vals['applyFillId'] = '1'
+
+ if st.number_format != style.DEFAULTS.number_format:
+ vals['numFmtId'] = '%d' % number_format_table[st.number_format]
+ vals['applyNumberFormat'] = '1'
+
+ if hash(st.alignment) != hash(style.DEFAULTS.alignment):
+ vals['applyAlignment'] = '1'
+
+ node = SubElement(cell_xfs, 'xf', vals)
+
+ if hash(st.alignment) != hash(style.DEFAULTS.alignment):
+ alignments = {}
+
+ for align_attr in ['horizontal','vertical']:
+ if hash(getattr(st.alignment, align_attr)) != hash(getattr(style.DEFAULTS.alignment, align_attr)):
+ alignments[align_attr] = getattr(st.alignment, align_attr)
+
+ SubElement(node, 'alignment', alignments)
+
+
+ def _write_cell_style(self):
+ cell_styles = SubElement(self._root, 'cellStyles', {'count':'1'})
+ cell_style = SubElement(cell_styles, 'cellStyle',
+ {'name':"Normal", 'xfId':"0", 'builtinId':"0"})
+
+ def _write_dxfs(self):
+ dxfs = SubElement(self._root, 'dxfs', {'count':'0'})
+
+ def _write_table_styles(self):
+
+ table_styles = SubElement(self._root, 'tableStyles',
+ {'count':'0', 'defaultTableStyle':'TableStyleMedium9',
+ 'defaultPivotStyle':'PivotStyleLight16'})
+
+ def _write_number_formats(self):
+
+ number_format_table = {}
+
+ number_format_list = []
+ exceptions_list = []
+ num_fmt_id = 165 # start at a greatly higher value as any builtin can go
+ num_fmt_offset = 0
+
+ for style in self._style_list:
+
+ if not style.number_format in number_format_list :
+ number_format_list.append(style.number_format)
+
+ for number_format in number_format_list:
+
+ if number_format.is_builtin():
+ btin = number_format.builtin_format_id(number_format.format_code)
+ number_format_table[number_format] = btin
+ else:
+ number_format_table[number_format] = num_fmt_id + num_fmt_offset
+ num_fmt_offset += 1
+ exceptions_list.append(number_format)
+
+ num_fmts = SubElement(self._root, 'numFmts',
+ {'count':'%d' % len(exceptions_list)})
+
+ for number_format in exceptions_list :
+ SubElement(num_fmts, 'numFmt',
+ {'numFmtId':'%d' % number_format_table[number_format],
+ 'formatCode':'%s' % number_format.format_code})
+
+ return number_format_table
diff --git a/tablib/packages/openpyxl/writer/theme.py b/tablib/packages/openpyxl/writer/theme.py
new file mode 100644
index 0000000..80700f2
--- /dev/null
+++ b/tablib/packages/openpyxl/writer/theme.py
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+# file openpyxl/writer/theme.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write the theme xml based on a fixed string."""
+
+# package imports
+from ..shared.xmltools import fromstring, get_document_content
+
+
+def write_theme():
+ """Write the theme xml."""
+ xml_node = fromstring(
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
+
+ '<a:theme xmlns:a="http://schemas.openxmlformats.org/'
+ 'drawingml/2006/main" name="Office Theme">'
+ '<a:themeElements>'
+
+ '<a:clrScheme name="Office">'
+ '<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>'
+ '<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>'
+ '<a:dk2><a:srgbClr val="1F497D"/></a:dk2>'
+ '<a:lt2><a:srgbClr val="EEECE1"/></a:lt2>'
+ '<a:accent1><a:srgbClr val="4F81BD"/></a:accent1>'
+ '<a:accent2><a:srgbClr val="C0504D"/></a:accent2>'
+ '<a:accent3><a:srgbClr val="9BBB59"/></a:accent3>'
+ '<a:accent4><a:srgbClr val="8064A2"/></a:accent4>'
+ '<a:accent5><a:srgbClr val="4BACC6"/></a:accent5>'
+ '<a:accent6><a:srgbClr val="F79646"/></a:accent6>'
+ '<a:hlink><a:srgbClr val="0000FF"/></a:hlink>'
+ '<a:folHlink><a:srgbClr val="800080"/></a:folHlink>'
+ '</a:clrScheme>'
+
+ '<a:fontScheme name="Office">'
+ '<a:majorFont>'
+ '<a:latin typeface="Cambria"/>'
+ '<a:ea typeface=""/>'
+ '<a:cs typeface=""/>'
+ '<a:font script="Jpan" typeface="MS Pゴシック"/>'
+ '<a:font script="Hang" typeface="맑은 고딕"/>'
+ '<a:font script="Hans" typeface="宋体"/>'
+ '<a:font script="Hant" typeface="新細明體"/>'
+ '<a:font script="Arab" typeface="Times New Roman"/>'
+ '<a:font script="Hebr" typeface="Times New Roman"/>'
+ '<a:font script="Thai" typeface="Tahoma"/>'
+ '<a:font script="Ethi" typeface="Nyala"/>'
+ '<a:font script="Beng" typeface="Vrinda"/>'
+ '<a:font script="Gujr" typeface="Shruti"/>'
+ '<a:font script="Khmr" typeface="MoolBoran"/>'
+ '<a:font script="Knda" typeface="Tunga"/>'
+ '<a:font script="Guru" typeface="Raavi"/>'
+ '<a:font script="Cans" typeface="Euphemia"/>'
+ '<a:font script="Cher" typeface="Plantagenet Cherokee"/>'
+ '<a:font script="Yiii" typeface="Microsoft Yi Baiti"/>'
+ '<a:font script="Tibt" typeface="Microsoft Himalaya"/>'
+ '<a:font script="Thaa" typeface="MV Boli"/>'
+ '<a:font script="Deva" typeface="Mangal"/>'
+ '<a:font script="Telu" typeface="Gautami"/>'
+ '<a:font script="Taml" typeface="Latha"/>'
+ '<a:font script="Syrc" typeface="Estrangelo Edessa"/>'
+ '<a:font script="Orya" typeface="Kalinga"/>'
+ '<a:font script="Mlym" typeface="Kartika"/>'
+ '<a:font script="Laoo" typeface="DokChampa"/>'
+ '<a:font script="Sinh" typeface="Iskoola Pota"/>'
+ '<a:font script="Mong" typeface="Mongolian Baiti"/>'
+ '<a:font script="Viet" typeface="Times New Roman"/>'
+ '<a:font script="Uigh" typeface="Microsoft Uighur"/>'
+ '</a:majorFont>'
+ '<a:minorFont>'
+ '<a:latin typeface="Calibri"/>'
+ '<a:ea typeface=""/>'
+ '<a:cs typeface=""/>'
+ '<a:font script="Jpan" typeface="MS Pゴシック"/>'
+ '<a:font script="Hang" typeface="맑은 고딕"/>'
+ '<a:font script="Hans" typeface="宋体"/>'
+ '<a:font script="Hant" typeface="新細明體"/>'
+ '<a:font script="Arab" typeface="Arial"/>'
+ '<a:font script="Hebr" typeface="Arial"/>'
+ '<a:font script="Thai" typeface="Tahoma"/>'
+ '<a:font script="Ethi" typeface="Nyala"/>'
+ '<a:font script="Beng" typeface="Vrinda"/>'
+ '<a:font script="Gujr" typeface="Shruti"/>'
+ '<a:font script="Khmr" typeface="DaunPenh"/>'
+ '<a:font script="Knda" typeface="Tunga"/>'
+ '<a:font script="Guru" typeface="Raavi"/>'
+ '<a:font script="Cans" typeface="Euphemia"/>'
+ '<a:font script="Cher" typeface="Plantagenet Cherokee"/>'
+ '<a:font script="Yiii" typeface="Microsoft Yi Baiti"/>'
+ '<a:font script="Tibt" typeface="Microsoft Himalaya"/>'
+ '<a:font script="Thaa" typeface="MV Boli"/>'
+ '<a:font script="Deva" typeface="Mangal"/>'
+ '<a:font script="Telu" typeface="Gautami"/>'
+ '<a:font script="Taml" typeface="Latha"/>'
+ '<a:font script="Syrc" typeface="Estrangelo Edessa"/>'
+ '<a:font script="Orya" typeface="Kalinga"/>'
+ '<a:font script="Mlym" typeface="Kartika"/>'
+ '<a:font script="Laoo" typeface="DokChampa"/>'
+ '<a:font script="Sinh" typeface="Iskoola Pota"/>'
+ '<a:font script="Mong" typeface="Mongolian Baiti"/>'
+ '<a:font script="Viet" typeface="Arial"/>'
+ '<a:font script="Uigh" typeface="Microsoft Uighur"/>'
+ '</a:minorFont>'
+ '</a:fontScheme>'
+
+ '<a:fmtScheme name="Office">'
+ '<a:fillStyleLst>'
+ '<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
+ '<a:gradFill rotWithShape="1"><a:gsLst>'
+ '<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/>'
+ '<a:satMod val="300000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/>'
+ '<a:satMod val="300000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/>'
+ '<a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst>'
+ '<a:lin ang="16200000" scaled="1"/></a:gradFill>'
+ '<a:gradFill rotWithShape="1"><a:gsLst>'
+ '<a:gs pos="0"><a:schemeClr val="phClr"><a:shade val="51000"/>'
+ '<a:satMod val="130000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="80000"><a:schemeClr val="phClr"><a:shade val="93000"/>'
+ '<a:satMod val="130000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="100000"><a:schemeClr val="phClr">'
+ '<a:shade val="94000"/>'
+ '<a:satMod val="135000"/></a:schemeClr></a:gs></a:gsLst>'
+ '<a:lin ang="16200000" scaled="0"/></a:gradFill></a:fillStyleLst>'
+ '<a:lnStyleLst>'
+ '<a:ln w="9525" cap="flat" cmpd="sng" algn="ctr">'
+ '<a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/>'
+ '<a:satMod val="105000"/></a:schemeClr></a:solidFill>'
+ '<a:prstDash val="solid"/></a:ln>'
+ '<a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill>'
+ '<a:schemeClr val="phClr"/></a:solidFill>'
+ '<a:prstDash val="solid"/></a:ln>'
+ '<a:ln w="38100" cap="flat" cmpd="sng" algn="ctr"><a:solidFill>'
+ '<a:schemeClr val="phClr"/></a:solidFill>'
+ '<a:prstDash val="solid"/></a:ln></a:lnStyleLst>'
+ '<a:effectStyleLst><a:effectStyle><a:effectLst>'
+ '<a:outerShdw blurRad="40000" dist="20000" dir="5400000" '
+ 'rotWithShape="0"><a:srgbClr val="000000">'
+ '<a:alpha val="38000"/></a:srgbClr></a:outerShdw></a:effectLst>'
+ '</a:effectStyle><a:effectStyle><a:effectLst>'
+ '<a:outerShdw blurRad="40000" dist="23000" dir="5400000" '
+ 'rotWithShape="0"><a:srgbClr val="000000">'
+ '<a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst>'
+ '</a:effectStyle><a:effectStyle><a:effectLst>'
+ '<a:outerShdw blurRad="40000" dist="23000" dir="5400000" '
+ 'rotWithShape="0"><a:srgbClr val="000000">'
+ '<a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst>'
+ '<a:scene3d><a:camera prst="orthographicFront">'
+ '<a:rot lat="0" lon="0" rev="0"/></a:camera>'
+ '<a:lightRig rig="threePt" dir="t">'
+ '<a:rot lat="0" lon="0" rev="1200000"/></a:lightRig>'
+ '</a:scene3d><a:sp3d><a:bevelT w="63500" h="25400"/>'
+ '</a:sp3d></a:effectStyle></a:effectStyleLst>'
+ '<a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/>'
+ '</a:solidFill><a:gradFill rotWithShape="1"><a:gsLst>'
+ '<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/>'
+ '<a:satMod val="350000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/>'
+ '<a:shade val="99000"/><a:satMod val="350000"/>'
+ '</a:schemeClr></a:gs>'
+ '<a:gs pos="100000"><a:schemeClr val="phClr">'
+ '<a:shade val="20000"/><a:satMod val="255000"/>'
+ '</a:schemeClr></a:gs></a:gsLst>'
+ '<a:path path="circle">'
+ '<a:fillToRect l="50000" t="-80000" r="50000" b="180000"/>'
+ '</a:path>'
+ '</a:gradFill><a:gradFill rotWithShape="1"><a:gsLst>'
+ '<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/>'
+ '<a:satMod val="300000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="100000"><a:schemeClr val="phClr">'
+ '<a:shade val="30000"/><a:satMod val="200000"/>'
+ '</a:schemeClr></a:gs></a:gsLst>'
+ '<a:path path="circle">'
+ '<a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path>'
+ '</a:gradFill></a:bgFillStyleLst></a:fmtScheme>'
+ '</a:themeElements>'
+ '<a:objectDefaults/><a:extraClrSchemeLst/>'
+ '</a:theme>')
+ return get_document_content(xml_node)
diff --git a/tablib/packages/openpyxl/writer/workbook.py b/tablib/packages/openpyxl/writer/workbook.py
new file mode 100644
index 0000000..e7b390c
--- /dev/null
+++ b/tablib/packages/openpyxl/writer/workbook.py
@@ -0,0 +1,204 @@
+# file openpyxl/writer/workbook.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write the workbook global settings to the archive."""
+
+# package imports
+from ..shared.xmltools import Element, SubElement
+from ..cell import absolute_coordinate
+from ..shared.xmltools import get_document_content
+from ..shared.ooxml import NAMESPACES, ARC_CORE, ARC_WORKBOOK, \
+ ARC_APP, ARC_THEME, ARC_STYLE, ARC_SHARED_STRINGS
+from ..shared.date_time import datetime_to_W3CDTF
+
+
+def write_properties_core(properties):
+ """Write the core properties to xml."""
+ root = Element('cp:coreProperties', {'xmlns:cp': NAMESPACES['cp'],
+ 'xmlns:xsi': NAMESPACES['xsi'], 'xmlns:dc': NAMESPACES['dc'],
+ 'xmlns:dcterms': NAMESPACES['dcterms'],
+ 'xmlns:dcmitype': NAMESPACES['dcmitype'], })
+ SubElement(root, 'dc:creator').text = properties.creator
+ SubElement(root, 'cp:lastModifiedBy').text = properties.last_modified_by
+ SubElement(root, 'dcterms:created', \
+ {'xsi:type': 'dcterms:W3CDTF'}).text = \
+ datetime_to_W3CDTF(properties.created)
+ SubElement(root, 'dcterms:modified',
+ {'xsi:type': 'dcterms:W3CDTF'}).text = \
+ datetime_to_W3CDTF(properties.modified)
+ return get_document_content(root)
+
+
+def write_content_types(workbook):
+ """Write the content-types xml."""
+ root = Element('Types', {'xmlns': 'http://schemas.openxmlformats.org/package/2006/content-types'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_THEME, 'ContentType': 'application/vnd.openxmlformats-officedocument.theme+xml'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_STYLE, 'ContentType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml'})
+ SubElement(root, 'Default', {'Extension': 'rels', 'ContentType': 'application/vnd.openxmlformats-package.relationships+xml'})
+ SubElement(root, 'Default', {'Extension': 'xml', 'ContentType': 'application/xml'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_WORKBOOK, 'ContentType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_APP, 'ContentType': 'application/vnd.openxmlformats-officedocument.extended-properties+xml'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_CORE, 'ContentType': 'application/vnd.openxmlformats-package.core-properties+xml'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_SHARED_STRINGS, 'ContentType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'})
+
+ drawing_id = 1
+ chart_id = 1
+
+ for sheet_id, sheet in enumerate(workbook.worksheets):
+ SubElement(root, 'Override',
+ {'PartName': '/xl/worksheets/sheet%d.xml' % (sheet_id + 1),
+ 'ContentType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'})
+ if sheet._charts:
+ SubElement(root, 'Override',
+ {'PartName' : '/xl/drawings/drawing%d.xml' % (sheet_id + 1),
+ 'ContentType' : 'application/vnd.openxmlformats-officedocument.drawing+xml'})
+ drawing_id += 1
+
+ for chart in sheet._charts:
+ SubElement(root, 'Override',
+ {'PartName' : '/xl/charts/chart%d.xml' % chart_id,
+ 'ContentType' : 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml'})
+ chart_id += 1
+ if chart._shapes:
+ SubElement(root, 'Override',
+ {'PartName' : '/xl/drawings/drawing%d.xml' % drawing_id,
+ 'ContentType' : 'application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml'})
+ drawing_id += 1
+
+ return get_document_content(root)
+
+
+def write_properties_app(workbook):
+ """Write the properties xml."""
+ worksheets_count = len(workbook.worksheets)
+ root = Element('Properties', {'xmlns': 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
+ 'xmlns:vt': 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'})
+ SubElement(root, 'Application').text = 'Microsoft Excel'
+ SubElement(root, 'DocSecurity').text = '0'
+ SubElement(root, 'ScaleCrop').text = 'false'
+ SubElement(root, 'Company')
+ SubElement(root, 'LinksUpToDate').text = 'false'
+ SubElement(root, 'SharedDoc').text = 'false'
+ SubElement(root, 'HyperlinksChanged').text = 'false'
+ SubElement(root, 'AppVersion').text = '12.0000'
+
+ # heading pairs part
+ heading_pairs = SubElement(root, 'HeadingPairs')
+ vector = SubElement(heading_pairs, 'vt:vector',
+ {'size': '2', 'baseType': 'variant'})
+ variant = SubElement(vector, 'vt:variant')
+ SubElement(variant, 'vt:lpstr').text = 'Worksheets'
+ variant = SubElement(vector, 'vt:variant')
+ SubElement(variant, 'vt:i4').text = '%d' % worksheets_count
+
+ # title of parts
+ title_of_parts = SubElement(root, 'TitlesOfParts')
+ vector = SubElement(title_of_parts, 'vt:vector',
+ {'size': '%d' % worksheets_count, 'baseType': 'lpstr'})
+ for ws in workbook.worksheets:
+ SubElement(vector, 'vt:lpstr').text = '%s' % ws.title
+ return get_document_content(root)
+
+
+def write_root_rels(workbook):
+ """Write the relationships xml."""
+ root = Element('Relationships', {'xmlns':
+ 'http://schemas.openxmlformats.org/package/2006/relationships'})
+ SubElement(root, 'Relationship', {'Id': 'rId1', 'Target': ARC_WORKBOOK,
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument'})
+ SubElement(root, 'Relationship', {'Id': 'rId2', 'Target': ARC_CORE,
+ 'Type': 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties'})
+ SubElement(root, 'Relationship', {'Id': 'rId3', 'Target': ARC_APP,
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties'})
+ return get_document_content(root)
+
+
+def write_workbook(workbook):
+ """Write the core workbook xml."""
+ root = Element('workbook', {'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
+ 'xml:space': 'preserve', 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'})
+ SubElement(root, 'fileVersion', {'appName': 'xl', 'lastEdited': '4',
+ 'lowestEdited': '4', 'rupBuild': '4505'})
+ SubElement(root, 'workbookPr', {'defaultThemeVersion': '124226',
+ 'codeName': 'ThisWorkbook'})
+ book_views = SubElement(root, 'bookViews')
+ SubElement(book_views, 'workbookView', {'activeTab': '%d' % workbook.get_index(workbook.get_active_sheet()),
+ 'autoFilterDateGrouping': '1', 'firstSheet': '0', 'minimized': '0',
+ 'showHorizontalScroll': '1', 'showSheetTabs': '1',
+ 'showVerticalScroll': '1', 'tabRatio': '600',
+ 'visibility': 'visible'})
+ # worksheets
+ sheets = SubElement(root, 'sheets')
+ for i, sheet in enumerate(workbook.worksheets):
+ sheet_node = SubElement(sheets, 'sheet', {'name': sheet.title,
+ 'sheetId': '%d' % (i + 1), 'r:id': 'rId%d' % (i + 1)})
+ if not sheet.sheet_state == sheet.SHEETSTATE_VISIBLE:
+ sheet_node.set('state', sheet.sheet_state)
+ # named ranges
+ defined_names = SubElement(root, 'definedNames')
+ for named_range in workbook.get_named_ranges():
+ name = SubElement(defined_names, 'definedName',
+ {'name': named_range.name})
+
+ # as there can be many cells in one range, generate the list of ranges
+ dest_cells = []
+ cell_ids = []
+ for worksheet, range_name in named_range.destinations:
+ cell_ids.append(workbook.get_index(worksheet))
+ dest_cells.append("'%s'!%s" % (worksheet.title.replace("'", "''"),
+ absolute_coordinate(range_name)))
+
+ # for local ranges, we must check all the cells belong to the same sheet
+ base_id = cell_ids[0]
+ if named_range.local_only and all([x == base_id for x in cell_ids]):
+ name.set('localSheetId', '%s' % base_id)
+
+ # finally write the cells list
+ name.text = ','.join(dest_cells)
+
+ SubElement(root, 'calcPr', {'calcId': '124519', 'calcMode': 'auto',
+ 'fullCalcOnLoad': '1'})
+ return get_document_content(root)
+
+
+def write_workbook_rels(workbook):
+ """Write the workbook relationships xml."""
+ root = Element('Relationships', {'xmlns':
+ 'http://schemas.openxmlformats.org/package/2006/relationships'})
+ for i in range(len(workbook.worksheets)):
+ SubElement(root, 'Relationship', {'Id': 'rId%d' % (i + 1),
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
+ 'Target': 'worksheets/sheet%s.xml' % (i + 1)})
+ rid = len(workbook.worksheets) + 1
+ SubElement(root, 'Relationship',
+ {'Id': 'rId%d' % rid, 'Target': 'sharedStrings.xml',
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings'})
+ SubElement(root, 'Relationship',
+ {'Id': 'rId%d' % (rid + 1), 'Target': 'styles.xml',
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'})
+ SubElement(root, 'Relationship',
+ {'Id': 'rId%d' % (rid + 2), 'Target': 'theme/theme1.xml',
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme'})
+ return get_document_content(root)
diff --git a/tablib/packages/openpyxl/writer/worksheet.py b/tablib/packages/openpyxl/writer/worksheet.py
new file mode 100644
index 0000000..91effe2
--- /dev/null
+++ b/tablib/packages/openpyxl/writer/worksheet.py
@@ -0,0 +1,209 @@
+# file openpyxl/writer/worksheet.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write worksheets to xml representations."""
+
+# Python stdlib imports
+from ....compat import BytesIO as StringIO # cStringIO doesn't handle unicode
+
+# package imports
+from ..cell import coordinate_from_string, column_index_from_string
+from ..shared.xmltools import Element, SubElement, XMLGenerator, \
+ get_document_content, start_tag, end_tag, tag
+
+
+def row_sort(cell):
+ """Translate column names for sorting."""
+ return column_index_from_string(cell.column)
+
+
+def write_worksheet(worksheet, string_table, style_table):
+ """Write a worksheet to an xml file."""
+ xml_file = StringIO()
+ doc = XMLGenerator(xml_file, 'utf-8')
+ start_tag(doc, 'worksheet',
+ {'xml:space': 'preserve',
+ 'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'})
+ start_tag(doc, 'sheetPr')
+ tag(doc, 'outlinePr',
+ {'summaryBelow': '%d' % (worksheet.show_summary_below),
+ 'summaryRight': '%d' % (worksheet.show_summary_right)})
+ end_tag(doc, 'sheetPr')
+ tag(doc, 'dimension', {'ref': '%s' % worksheet.calculate_dimension()})
+ write_worksheet_sheetviews(doc, worksheet)
+ tag(doc, 'sheetFormatPr', {'defaultRowHeight': '15'})
+ write_worksheet_cols(doc, worksheet)
+ write_worksheet_data(doc, worksheet, string_table, style_table)
+ if worksheet.auto_filter:
+ tag(doc, 'autoFilter', {'ref': worksheet.auto_filter})
+ write_worksheet_hyperlinks(doc, worksheet)
+ if worksheet._charts:
+ tag(doc, 'drawing', {'r:id':'rId1'})
+ end_tag(doc, 'worksheet')
+ doc.endDocument()
+ xml_string = xml_file.getvalue()
+ xml_file.close()
+ return xml_string
+
+def write_worksheet_sheetviews(doc, worksheet):
+ start_tag(doc, 'sheetViews')
+ start_tag(doc, 'sheetView', {'workbookViewId': '0'})
+ selectionAttrs = {}
+ topLeftCell = worksheet.freeze_panes
+ if topLeftCell:
+ colName, row = coordinate_from_string(topLeftCell)
+ column = column_index_from_string(colName)
+ pane = 'topRight'
+ paneAttrs = {}
+ if column > 1:
+ paneAttrs['xSplit'] = str(column - 1)
+ if row > 1:
+ paneAttrs['ySplit'] = str(row - 1)
+ pane = 'bottomLeft'
+ if column > 1:
+ pane = 'bottomRight'
+ paneAttrs.update(dict(topLeftCell=topLeftCell,
+ activePane=pane,
+ state='frozen'))
+ tag(doc, 'pane', paneAttrs)
+ selectionAttrs['pane'] = pane
+ if row > 1 and column > 1:
+ tag(doc, 'selection', {'pane': 'topRight'})
+ tag(doc, 'selection', {'pane': 'bottomLeft'})
+
+ selectionAttrs.update({'activeCell': worksheet.active_cell,
+ 'sqref': worksheet.selected_cell})
+
+ tag(doc, 'selection', selectionAttrs)
+ end_tag(doc, 'sheetView')
+ end_tag(doc, 'sheetViews')
+
+
+def write_worksheet_cols(doc, worksheet):
+ """Write worksheet columns to xml."""
+ if worksheet.column_dimensions:
+ start_tag(doc, 'cols')
+ for column_string, columndimension in \
+ worksheet.column_dimensions.items():
+ col_index = column_index_from_string(column_string)
+ col_def = {}
+ col_def['collapsed'] = str(columndimension.style_index)
+ col_def['min'] = str(col_index)
+ col_def['max'] = str(col_index)
+ if columndimension.width != \
+ worksheet.default_column_dimension.width:
+ col_def['customWidth'] = 'true'
+ if not columndimension.visible:
+ col_def['hidden'] = 'true'
+ if columndimension.outline_level > 0:
+ col_def['outlineLevel'] = str(columndimension.outline_level)
+ if columndimension.collapsed:
+ col_def['collapsed'] = 'true'
+ if columndimension.auto_size:
+ col_def['bestFit'] = 'true'
+ if columndimension.width > 0:
+ col_def['width'] = str(columndimension.width)
+ else:
+ col_def['width'] = '9.10'
+ tag(doc, 'col', col_def)
+ end_tag(doc, 'cols')
+
+
+def write_worksheet_data(doc, worksheet, string_table, style_table):
+ """Write worksheet data to xml."""
+ start_tag(doc, 'sheetData')
+ max_column = worksheet.get_highest_column()
+ style_id_by_hash = style_table
+ cells_by_row = {}
+ for cell in worksheet.get_cell_collection():
+ cells_by_row.setdefault(cell.row, []).append(cell)
+ for row_idx in sorted(cells_by_row):
+ row_dimension = worksheet.row_dimensions[row_idx]
+ attrs = {'r': '%d' % row_idx,
+ 'spans': '1:%d' % max_column}
+ if row_dimension.height > 0:
+ attrs['ht'] = str(row_dimension.height)
+ attrs['customHeight'] = '1'
+ start_tag(doc, 'row', attrs)
+ row_cells = cells_by_row[row_idx]
+ sorted_cells = sorted(row_cells, key = row_sort)
+ for cell in sorted_cells:
+ value = cell._value
+ coordinate = cell.get_coordinate()
+ attributes = {'r': coordinate}
+ attributes['t'] = cell.data_type
+ if coordinate in worksheet._styles:
+ attributes['s'] = '%d' % style_id_by_hash[
+ hash(worksheet._styles[coordinate])]
+ start_tag(doc, 'c', attributes)
+ if value is None:
+ tag(doc, 'v', body='')
+ elif cell.data_type == cell.TYPE_STRING:
+ tag(doc, 'v', body = '%s' % string_table[value])
+ elif cell.data_type == cell.TYPE_FORMULA:
+ tag(doc, 'f', body = '%s' % value[1:])
+ tag(doc, 'v')
+ elif cell.data_type == cell.TYPE_NUMERIC:
+ tag(doc, 'v', body = '%s' % value)
+ else:
+ tag(doc, 'v', body = '%s' % value)
+ end_tag(doc, 'c')
+ end_tag(doc, 'row')
+ end_tag(doc, 'sheetData')
+
+
+def write_worksheet_hyperlinks(doc, worksheet):
+ """Write worksheet hyperlinks to xml."""
+ write_hyperlinks = False
+ for cell in worksheet.get_cell_collection():
+ if cell.hyperlink_rel_id is not None:
+ write_hyperlinks = True
+ break
+ if write_hyperlinks:
+ start_tag(doc, 'hyperlinks')
+ for cell in worksheet.get_cell_collection():
+ if cell.hyperlink_rel_id is not None:
+ attrs = {'display': cell.hyperlink,
+ 'ref': cell.get_coordinate(),
+ 'r:id': cell.hyperlink_rel_id}
+ tag(doc, 'hyperlink', attrs)
+ end_tag(doc, 'hyperlinks')
+
+
+def write_worksheet_rels(worksheet, idx):
+ """Write relationships for the worksheet to xml."""
+ root = Element('Relationships', {'xmlns': 'http://schemas.openxmlformats.org/package/2006/relationships'})
+ for rel in worksheet.relationships:
+ attrs = {'Id': rel.id, 'Type': rel.type, 'Target': rel.target}
+ if rel.target_mode:
+ attrs['TargetMode'] = rel.target_mode
+ SubElement(root, 'Relationship', attrs)
+ if worksheet._charts:
+ attrs = {'Id' : 'rId1',
+ 'Type' : 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
+ 'Target' : '../drawings/drawing%s.xml' % idx }
+ SubElement(root, 'Relationship', attrs)
+ return get_document_content(root)
diff --git a/tablib/packages/openpyxl3/__init__.py b/tablib/packages/openpyxl3/__init__.py
new file mode 100644
index 0000000..81381d7
--- /dev/null
+++ b/tablib/packages/openpyxl3/__init__.py
@@ -0,0 +1,53 @@
+# file openpyxl/__init__.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Imports for the openpyxl package."""
+
+# package imports
+from . import cell
+from . import namedrange
+from . import style
+from . import workbook
+from . import worksheet
+from . import reader
+from . import shared
+from . import writer
+
+# constants
+
+__major__ = 1 # for major interface/format changes
+__minor__ = 5 # for minor interface/format changes
+__release__ = 2 # for tweaks, bug-fixes, or development
+
+__version__ = '%d.%d.%d' % (__major__, __minor__, __release__)
+
+__author__ = 'Eric Gazoni'
+__license__ = 'MIT/Expat'
+__author_email__ = 'eric.gazoni@gmail.com'
+__maintainer_email__ = 'openpyxl-users@googlegroups.com'
+__url__ = 'http://bitbucket.org/ericgazoni/openpyxl/wiki/Home'
+__downloadUrl__ = "http://bitbucket.org/ericgazoni/openpyxl/downloads"
+
+__all__ = ('reader', 'shared', 'writer',)
diff --git a/tablib/packages/openpyxl3/cell.py b/tablib/packages/openpyxl3/cell.py
new file mode 100644
index 0000000..1171fde
--- /dev/null
+++ b/tablib/packages/openpyxl3/cell.py
@@ -0,0 +1,384 @@
+# file openpyxl/cell.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Manage individual cells in a spreadsheet.
+
+The Cell class is required to know its value and type, display options,
+and any other features of an Excel cell. Utilities for referencing
+cells using Excel's 'A1' column/row nomenclature are also provided.
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+# Python stdlib imports
+import datetime
+import re
+
+# package imports
+from .shared.date_time import SharedDate
+from .shared.exc import CellCoordinatesException, \
+ ColumnStringIndexException, DataTypeException
+from .style import NumberFormat
+
+# constants
+COORD_RE = re.compile('^[$]?([A-Z]+)[$]?(\d+)$')
+
+ABSOLUTE_RE = re.compile('^[$]?([A-Z]+)[$]?(\d+)(:[$]?([A-Z]+)[$]?(\d+))?$')
+
+def coordinate_from_string(coord_string):
+ """Convert a coordinate string like 'B12' to a tuple ('B', 12)"""
+ match = COORD_RE.match(coord_string.upper())
+ if not match:
+ msg = 'Invalid cell coordinates (%s)' % coord_string
+ raise CellCoordinatesException(msg)
+ column, row = match.groups()
+ return (column, int(row))
+
+
+def absolute_coordinate(coord_string):
+ """Convert a coordinate to an absolute coordinate string (B12 -> $B$12)"""
+ parts = ABSOLUTE_RE.match(coord_string).groups()
+
+ if all(parts[-2:]):
+ return '$%s$%s:$%s$%s' % (parts[0], parts[1], parts[3], parts[4])
+ else:
+ return '$%s$%s' % (parts[0], parts[1])
+
+
+def column_index_from_string(column, fast = False):
+ """Convert a column letter into a column number (e.g. B -> 2)
+
+ Excel only supports 1-3 letter column names from A -> ZZZ, so we
+ restrict our column names to 1-3 characters, each in the range A-Z.
+
+ .. note::
+
+ Fast mode is faster but does not check that all letters are capitals between A and Z
+
+ """
+ column = column.upper()
+
+ clen = len(column)
+
+ if not fast and not all('A' <= char <= 'Z' for char in column):
+ msg = 'Column string must contain only characters A-Z: got %s' % column
+ raise ColumnStringIndexException(msg)
+
+ if clen == 1:
+ return ord(column[0]) - 64
+ elif clen == 2:
+ return ((1 + (ord(column[0]) - 65)) * 26) + (ord(column[1]) - 64)
+ elif clen == 3:
+ return ((1 + (ord(column[0]) - 65)) * 676) + ((1 + (ord(column[1]) - 65)) * 26) + (ord(column[2]) - 64)
+ elif clen > 3:
+ raise ColumnStringIndexException('Column string index can not be longer than 3 characters')
+ else:
+ raise ColumnStringIndexException('Column string index can not be empty')
+
+
+def get_column_letter(col_idx):
+ """Convert a column number into a column letter (3 -> 'C')
+
+ Right shift the column col_idx by 26 to find column letters in reverse
+ order. These numbers are 1-based, and can be converted to ASCII
+ ordinals by adding 64.
+
+ """
+ # these indicies corrospond to A -> ZZZ and include all allowed
+ # columns
+ if not 1 <= col_idx <= 18278:
+ msg = 'Column index out of bounds: %s' % col_idx
+ raise ColumnStringIndexException(msg)
+ ordinals = []
+ temp = col_idx
+ while temp:
+ quotient, remainder = divmod(temp, 26)
+ # check for exact division and borrow if needed
+ if remainder == 0:
+ quotient -= 1
+ remainder = 26
+ ordinals.append(remainder + 64)
+ temp = quotient
+ ordinals.reverse()
+ return ''.join([chr(ordinal) for ordinal in ordinals])
+
+
+class Cell(object):
+ """Describes cell associated properties.
+
+ Properties of interest include style, type, value, and address.
+
+ """
+ __slots__ = ('column',
+ 'row',
+ '_value',
+ '_data_type',
+ 'parent',
+ 'xf_index',
+ '_hyperlink_rel')
+
+ ERROR_CODES = {'#NULL!': 0,
+ '#DIV/0!': 1,
+ '#VALUE!': 2,
+ '#REF!': 3,
+ '#NAME?': 4,
+ '#NUM!': 5,
+ '#N/A': 6}
+
+ TYPE_STRING = 's'
+ TYPE_FORMULA = 'f'
+ TYPE_NUMERIC = 'n'
+ TYPE_BOOL = 'b'
+ TYPE_NULL = 's'
+ TYPE_INLINE = 'inlineStr'
+ TYPE_ERROR = 'e'
+
+ VALID_TYPES = [TYPE_STRING, TYPE_FORMULA, TYPE_NUMERIC, TYPE_BOOL,
+ TYPE_NULL, TYPE_INLINE, TYPE_ERROR]
+
+ RE_PATTERNS = {
+ 'percentage': re.compile('^\-?[0-9]*\.?[0-9]*\s?\%$'),
+ 'time': re.compile('^(\d|[0-1]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$'),
+ 'numeric': re.compile('^\-?([0-9]+\\.?[0-9]*|[0-9]*\\.?[0-9]+)((E|e)\-?[0-9]+)?$'), }
+
+ def __init__(self, worksheet, column, row, value = None):
+ self.column = column.upper()
+ self.row = row
+ # _value is the stored value, while value is the displayed value
+ self._value = None
+ self._hyperlink_rel = None
+ self._data_type = self.TYPE_NULL
+ if value:
+ self.value = value
+ self.parent = worksheet
+ self.xf_index = 0
+
+ def __repr__(self):
+ return "<Cell %s.%s>" % (self.parent.title, self.get_coordinate())
+
+ def check_string(self, value):
+ """Check string coding, length, and line break character"""
+ # convert to unicode string
+ value = str(value)
+ # string must never be longer than 32,767 characters
+ # truncate if necessary
+ value = value[:32767]
+ # we require that newline is represented as "\n" in core,
+ # not as "\r\n" or "\r"
+ value = value.replace('\r\n', '\n')
+ return value
+
+ def check_numeric(self, value):
+ """Cast value to int or float if necessary"""
+ if not isinstance(value, (int, float)):
+ try:
+ value = int(value)
+ except ValueError:
+ value = float(value)
+ return value
+
+ def set_value_explicit(self, value = None, data_type = TYPE_STRING):
+ """Coerce values according to their explicit type"""
+ type_coercion_map = {
+ self.TYPE_INLINE: self.check_string,
+ self.TYPE_STRING: self.check_string,
+ self.TYPE_FORMULA: str,
+ self.TYPE_NUMERIC: self.check_numeric,
+ self.TYPE_BOOL: bool, }
+ try:
+ self._value = type_coercion_map[data_type](value)
+ except KeyError:
+ if data_type not in self.VALID_TYPES:
+ msg = 'Invalid data type: %s' % data_type
+ raise DataTypeException(msg)
+ self._data_type = data_type
+
+ def data_type_for_value(self, value):
+ """Given a value, infer the correct data type"""
+ if value is None:
+ data_type = self.TYPE_NULL
+ elif value is True or value is False:
+ data_type = self.TYPE_BOOL
+ elif isinstance(value, (int, float)):
+ data_type = self.TYPE_NUMERIC
+ elif not value:
+ data_type = self.TYPE_STRING
+ elif isinstance(value, (datetime.datetime, datetime.date)):
+ data_type = self.TYPE_NUMERIC
+ elif isinstance(value, str) and value[0] == '=':
+ data_type = self.TYPE_FORMULA
+ elif self.RE_PATTERNS['numeric'].match(value):
+ data_type = self.TYPE_NUMERIC
+ elif value.strip() in self.ERROR_CODES:
+ data_type = self.TYPE_ERROR
+ else:
+ data_type = self.TYPE_STRING
+ return data_type
+
+ def bind_value(self, value):
+ """Given a value, infer type and display options."""
+ self._data_type = self.data_type_for_value(value)
+ if value is None:
+ self.set_value_explicit('', self.TYPE_NULL)
+ return True
+ elif self._data_type == self.TYPE_STRING:
+ # percentage detection
+ percentage_search = self.RE_PATTERNS['percentage'].match(value)
+ if percentage_search and value.strip() != '%':
+ value = float(value.replace('%', '')) / 100.0
+ self.set_value_explicit(value, self.TYPE_NUMERIC)
+ self._set_number_format(NumberFormat.FORMAT_PERCENTAGE)
+ return True
+ # time detection
+ time_search = self.RE_PATTERNS['time'].match(value)
+ if time_search:
+ sep_count = value.count(':') #pylint: disable-msg=E1103
+ if sep_count == 1:
+ hours, minutes = [int(bit) for bit in value.split(':')] #pylint: disable-msg=E1103
+ seconds = 0
+ elif sep_count == 2:
+ hours, minutes, seconds = \
+ [int(bit) for bit in value.split(':')] #pylint: disable-msg=E1103
+ days = (hours / 24.0) + (minutes / 1440.0) + \
+ (seconds / 86400.0)
+ self.set_value_explicit(days, self.TYPE_NUMERIC)
+ self._set_number_format(NumberFormat.FORMAT_DATE_TIME3)
+ return True
+ if self._data_type == self.TYPE_NUMERIC:
+ # date detection
+ # if the value is a date, but not a date time, make it a
+ # datetime, and set the time part to 0
+ if isinstance(value, datetime.date) and not \
+ isinstance(value, datetime.datetime):
+ value = datetime.datetime.combine(value, datetime.time())
+ if isinstance(value, datetime.datetime):
+ value = SharedDate().datetime_to_julian(date = value)
+ self.set_value_explicit(value, self.TYPE_NUMERIC)
+ self._set_number_format(NumberFormat.FORMAT_DATE_YYYYMMDD2)
+ return True
+ self.set_value_explicit(value, self._data_type)
+
+ def _get_value(self):
+ """Return the value, formatted as a date if needed"""
+ value = self._value
+ if self.is_date():
+ value = SharedDate().from_julian(value)
+ return value
+
+ def _set_value(self, value):
+ """Set the value and infer type and display options."""
+ self.bind_value(value)
+
+ value = property(_get_value, _set_value,
+ doc = 'Get or set the value held in the cell.\n\n'
+ ':rtype: depends on the value (string, float, int or '
+ ':class:`datetime.datetime`)')
+
+ def _set_hyperlink(self, val):
+ """Set value and display for hyperlinks in a cell"""
+ if self._hyperlink_rel is None:
+ self._hyperlink_rel = self.parent.create_relationship("hyperlink")
+ self._hyperlink_rel.target = val
+ self._hyperlink_rel.target_mode = "External"
+ if self._value is None:
+ self.value = val
+
+ def _get_hyperlink(self):
+ """Return the hyperlink target or an empty string"""
+ return self._hyperlink_rel is not None and \
+ self._hyperlink_rel.target or ''
+
+ hyperlink = property(_get_hyperlink, _set_hyperlink,
+ doc = 'Get or set the hyperlink held in the cell. '
+ 'Automatically sets the `value` of the cell with link text, '
+ 'but you can modify it afterwards by setting the '
+ '`value` property, and the hyperlink will remain.\n\n'
+ ':rtype: string')
+
+ @property
+ def hyperlink_rel_id(self):
+ """Return the id pointed to by the hyperlink, or None"""
+ return self._hyperlink_rel is not None and \
+ self._hyperlink_rel.id or None
+
+ def _set_number_format(self, format_code):
+ """Set a new formatting code for numeric values"""
+ self.style.number_format.format_code = format_code
+
+ @property
+ def has_style(self):
+ """Check if the parent worksheet has a style for this cell"""
+ return self.get_coordinate() in self.parent._styles #pylint: disable-msg=W0212
+
+ @property
+ def style(self):
+ """Returns the :class:`.style.Style` object for this cell"""
+ return self.parent.get_style(self.get_coordinate())
+
+ @property
+ def data_type(self):
+ """Return the data type represented by this cell"""
+ return self._data_type
+
+ def get_coordinate(self):
+ """Return the coordinate string for this cell (e.g. 'B12')
+
+ :rtype: string
+ """
+ return '%s%s' % (self.column, self.row)
+
+ @property
+ def address(self):
+ """Return the coordinate string for this cell (e.g. 'B12')
+
+ :rtype: string
+ """
+ return self.get_coordinate()
+
+ def offset(self, row = 0, column = 0):
+ """Returns a cell location relative to this cell.
+
+ :param row: number of rows to offset
+ :type row: int
+
+ :param column: number of columns to offset
+ :type column: int
+
+ :rtype: :class:`.cell.Cell`
+ """
+ offset_column = get_column_letter(column_index_from_string(
+ column = self.column) + column)
+ offset_row = self.row + row
+ return self.parent.cell('%s%s' % (offset_column, offset_row))
+
+ def is_date(self):
+ """Returns whether the value is *probably* a date or not
+
+ :rtype: bool
+ """
+ return (self.has_style
+ and self.style.number_format.is_date_format()
+ and isinstance(self._value, (int, float)))
diff --git a/tablib/packages/openpyxl3/chart.py b/tablib/packages/openpyxl3/chart.py
new file mode 100644
index 0000000..265ccaf
--- /dev/null
+++ b/tablib/packages/openpyxl3/chart.py
@@ -0,0 +1,340 @@
+'''
+Copyright (c) 2010 openpyxl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+@license: http://www.opensource.org/licenses/mit-license.php
+@author: Eric Gazoni
+'''
+
+import math
+
+from .style import NumberFormat
+from .drawing import Drawing, Shape
+from .shared.units import pixels_to_EMU, short_color
+from .cell import get_column_letter
+
+class Axis(object):
+
+ POSITION_BOTTOM = 'b'
+ POSITION_LEFT = 'l'
+
+ ORIENTATION_MIN_MAX = "minMax"
+
+ def __init__(self):
+
+ self.orientation = self.ORIENTATION_MIN_MAX
+ self.number_format = NumberFormat()
+ for attr in ('position','tick_label_position','crosses',
+ 'auto','label_align','label_offset','cross_between'):
+ setattr(self, attr, None)
+ self.min = 0
+ self.max = None
+ self.unit = None
+
+ @classmethod
+ def default_category(cls):
+ """ default values for category axes """
+
+ ax = Axis()
+ ax.id = 60871424
+ ax.cross = 60873344
+ ax.position = Axis.POSITION_BOTTOM
+ ax.tick_label_position = 'nextTo'
+ ax.crosses = "autoZero"
+ ax.auto = True
+ ax.label_align = 'ctr'
+ ax.label_offset = 100
+ return ax
+
+ @classmethod
+ def default_value(cls):
+ """ default values for value axes """
+
+ ax = Axis()
+ ax.id = 60873344
+ ax.cross = 60871424
+ ax.position = Axis.POSITION_LEFT
+ ax.major_gridlines = None
+ ax.tick_label_position = 'nextTo'
+ ax.crosses = 'autoZero'
+ ax.auto = False
+ ax.cross_between = 'between'
+ return ax
+
+class Reference(object):
+ """ a simple wrapper around a serie of reference data """
+
+ def __init__(self, sheet, pos1, pos2=None):
+
+ self.sheet = sheet
+ self.pos1 = pos1
+ self.pos2 = pos2
+
+ def get_type(self):
+
+ if isinstance(self.cache[0], str):
+ return 'str'
+ else:
+ return 'num'
+
+ def _get_ref(self):
+ """ format excel reference notation """
+
+ if self.pos2:
+ return '%s!$%s$%s:$%s$%s' % (self.sheet.title,
+ get_column_letter(self.pos1[1]+1), self.pos1[0]+1,
+ get_column_letter(self.pos2[1]+1), self.pos2[0]+1)
+ else:
+ return '%s!$%s$%s' % (self.sheet.title,
+ get_column_letter(self.pos1[1]+1), self.pos1[0]+1)
+
+
+ def _get_cache(self):
+ """ read data in sheet - to be used at writing time """
+
+ cache = []
+ if self.pos2:
+ for row in range(self.pos1[0], self.pos2[0]+1):
+ for col in range(self.pos1[1], self.pos2[1]+1):
+ cache.append(self.sheet.cell(row=row, column=col).value)
+ else:
+ cell = self.sheet.cell(row=self.pos1[0], column=self.pos1[1])
+ cache.append(cell.value)
+ return cache
+
+
+class Serie(object):
+ """ a serie of data and possibly associated labels """
+
+ MARKER_NONE = 'none'
+
+ def __init__(self, values, labels=None, legend=None, color=None, xvalues=None):
+
+ self.marker = Serie.MARKER_NONE
+ self.values = values
+ self.xvalues = xvalues
+ self.labels = labels
+ self.legend = legend
+ self.error_bar = None
+ self._color = color
+
+ def _get_color(self):
+ return self._color
+
+ def _set_color(self, color):
+ self._color = short_color(color)
+
+ color = property(_get_color, _set_color)
+
+ def get_min_max(self):
+
+ if self.error_bar:
+ err_cache = self.error_bar.values._get_cache()
+ vals = [v + err_cache[i] \
+ for i,v in enumerate(self.values._get_cache())]
+ else:
+ vals = self.values._get_cache()
+ return min(vals), max(vals)
+
+ def __len__(self):
+
+ return len(self.values.cache)
+
+class Legend(object):
+
+ def __init__(self):
+
+ self.position = 'r'
+ self.layout = None
+
+class ErrorBar(object):
+
+ PLUS = 1
+ MINUS = 2
+ PLUS_MINUS = 3
+
+ def __init__(self, _type, values):
+
+ self.type = _type
+ self.values = values
+
+class Chart(object):
+ """ raw chart class """
+
+ GROUPING_CLUSTERED = 'clustered'
+ GROUPING_STANDARD = 'standard'
+
+ BAR_CHART = 1
+ LINE_CHART = 2
+ SCATTER_CHART = 3
+
+ def __init__(self, _type, grouping):
+
+ self._series = []
+
+ # public api
+ self.type = _type
+ self.grouping = grouping
+ self.x_axis = Axis.default_category()
+ self.y_axis = Axis.default_value()
+ self.legend = Legend()
+ self.lang = 'fr-FR'
+ self.title = ''
+ self.print_margins = dict(b=.75, l=.7, r=.7, t=.75, header=0.3, footer=.3)
+
+ # the containing drawing
+ self.drawing = Drawing()
+
+ # the offset for the plot part in percentage of the drawing size
+ self.width = .6
+ self.height = .6
+ self.margin_top = self._get_max_margin_top()
+ self.margin_left = 0
+
+ # the user defined shapes
+ self._shapes = []
+
+ def add_serie(self, serie):
+
+ serie.id = len(self._series)
+ self._series.append(serie)
+ self._compute_min_max()
+ if not None in [s.xvalues for s in self._series]:
+ self._compute_xmin_xmax()
+
+ def add_shape(self, shape):
+
+ shape._chart = self
+ self._shapes.append(shape)
+
+ def get_x_units(self):
+ """ calculate one unit for x axis in EMU """
+
+ return max([len(s.values._get_cache()) for s in self._series])
+
+ def get_y_units(self):
+ """ calculate one unit for y axis in EMU """
+
+ dh = pixels_to_EMU(self.drawing.height)
+ return (dh * self.height) / self.y_axis.max
+
+ def get_y_chars(self):
+ """ estimate nb of chars for y axis """
+
+ _max = max([max(s.values._get_cache()) for s in self._series])
+ return len(str(int(_max)))
+
+ def _compute_min_max(self):
+ """ compute y axis limits and units """
+
+ maxi = max([max(s.values._get_cache()) for s in self._series])
+
+ mul = None
+ if maxi < 1:
+ s = str(maxi).split('.')[1]
+ mul = 10
+ for x in s:
+ if x == '0':
+ mul *= 10
+ else:
+ break
+ maxi = maxi * mul
+
+ maxi = math.ceil(maxi * 1.1)
+ sz = len(str(int(maxi))) - 1
+ unit = math.ceil(math.ceil(maxi / pow(10, sz)) * pow(10, sz-1))
+ maxi = math.ceil(maxi/unit) * unit
+
+ if mul is not None:
+ maxi = maxi/mul
+ unit = unit/mul
+
+ if maxi / unit > 9:
+ # no more that 10 ticks
+ unit *= 2
+
+ self.y_axis.max = maxi
+ self.y_axis.unit = unit
+
+ def _compute_xmin_xmax(self):
+ """ compute x axis limits and units """
+
+ maxi = max([max(s.xvalues._get_cache()) for s in self._series])
+
+ mul = None
+ if maxi < 1:
+ s = str(maxi).split('.')[1]
+ mul = 10
+ for x in s:
+ if x == '0':
+ mul *= 10
+ else:
+ break
+ maxi = maxi * mul
+
+ maxi = math.ceil(maxi * 1.1)
+ sz = len(str(int(maxi))) - 1
+ unit = math.ceil(math.ceil(maxi / pow(10, sz)) * pow(10, sz-1))
+ maxi = math.ceil(maxi/unit) * unit
+
+ if mul is not None:
+ maxi = maxi/mul
+ unit = unit/mul
+
+ if maxi / unit > 9:
+ # no more that 10 ticks
+ unit *= 2
+
+ self.x_axis.max = maxi
+ self.x_axis.unit = unit
+
+ def _get_max_margin_top(self):
+
+ mb = Shape.FONT_HEIGHT + Shape.MARGIN_BOTTOM
+ plot_height = self.drawing.height * self.height
+ return float(self.drawing.height - plot_height - mb)/self.drawing.height
+
+ def _get_min_margin_left(self):
+
+ ml = (self.get_y_chars() * Shape.FONT_WIDTH) + Shape.MARGIN_LEFT
+ return float(ml)/self.drawing.width
+
+ def _get_margin_top(self):
+ """ get margin in percent """
+
+ return min(self.margin_top, self._get_max_margin_top())
+
+ def _get_margin_left(self):
+
+ return max(self._get_min_margin_left(), self.margin_left)
+
+class BarChart(Chart):
+ def __init__(self):
+ super(BarChart, self).__init__(Chart.BAR_CHART, Chart.GROUPING_CLUSTERED)
+
+class LineChart(Chart):
+ def __init__(self):
+ super(LineChart, self).__init__(Chart.LINE_CHART, Chart.GROUPING_STANDARD)
+
+class ScatterChart(Chart):
+ def __init__(self):
+ super(ScatterChart, self).__init__(Chart.SCATTER_CHART, Chart.GROUPING_STANDARD)
+
+
diff --git a/tablib/packages/openpyxl3/drawing.py b/tablib/packages/openpyxl3/drawing.py
new file mode 100644
index 0000000..0007569
--- /dev/null
+++ b/tablib/packages/openpyxl3/drawing.py
@@ -0,0 +1,402 @@
+'''
+Copyright (c) 2010 openpyxl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+@license: http://www.opensource.org/licenses/mit-license.php
+@author: Eric Gazoni
+'''
+
+import math
+from .style import Color
+from .shared.units import pixels_to_EMU, EMU_to_pixels, short_color
+
+class Shadow(object):
+
+ SHADOW_BOTTOM = 'b'
+ SHADOW_BOTTOM_LEFT = 'bl'
+ SHADOW_BOTTOM_RIGHT = 'br'
+ SHADOW_CENTER = 'ctr'
+ SHADOW_LEFT = 'l'
+ SHADOW_TOP = 't'
+ SHADOW_TOP_LEFT = 'tl'
+ SHADOW_TOP_RIGHT = 'tr'
+
+ def __init__(self):
+ self.visible = False
+ self.blurRadius = 6
+ self.distance = 2
+ self.direction = 0
+ self.alignment = self.SHADOW_BOTTOM_RIGHT
+ self.color = Color(Color.BLACK)
+ self.alpha = 50
+
+class Drawing(object):
+ """ a drawing object - eg container for shapes or charts
+ we assume user specifies dimensions in pixels; units are
+ converted to EMU in the drawing part
+ """
+
+ count = 0
+
+ def __init__(self):
+
+ self.name = ''
+ self.description = ''
+ self.coordinates = ((1,2), (16,8))
+ self.left = 0
+ self.top = 0
+ self._width = EMU_to_pixels(200000)
+ self._height = EMU_to_pixels(1828800)
+ self.resize_proportional = False
+ self.rotation = 0
+# self.shadow = Shadow()
+
+ def _set_width(self, w):
+
+ if self.resize_proportional and w:
+ ratio = self._height / self._width
+ self._height = round(ratio * w)
+ self._width = w
+
+ def _get_width(self):
+
+ return self._width
+
+ width = property(_get_width, _set_width)
+
+ def _set_height(self, h):
+
+ if self.resize_proportional and h:
+ ratio = self._width / self._height
+ self._width = round(ratio * h)
+ self._height = h
+
+ def _get_height(self):
+
+ return self._height
+
+ height = property(_get_height, _set_height)
+
+ def set_dimension(self, w=0, h=0):
+
+ xratio = w / self._width
+ yratio = h / self._height
+
+ if self.resize_proportional and w and h:
+ if (xratio * self._height) < h:
+ self._height = math.ceil(xratio * self._height)
+ self._width = width
+ else:
+ self._width = math.ceil(yratio * self._width)
+ self._height = height
+
+ def get_emu_dimensions(self):
+ """ return (x, y, w, h) in EMU """
+
+ return (pixels_to_EMU(self.left), pixels_to_EMU(self.top),
+ pixels_to_EMU(self._width), pixels_to_EMU(self._height))
+
+
+class Shape(object):
+ """ a drawing inside a chart
+ coordiantes are specified by the user in the axis units
+ """
+
+ MARGIN_LEFT = 6 + 13 + 1
+ MARGIN_BOTTOM = 17 + 11
+
+ FONT_WIDTH = 7
+ FONT_HEIGHT = 8
+
+ ROUND_RECT = 'roundRect'
+ RECT = 'rect'
+
+ # other shapes to define :
+ '''
+ "line"
+ "lineInv"
+ "triangle"
+ "rtTriangle"
+ "diamond"
+ "parallelogram"
+ "trapezoid"
+ "nonIsoscelesTrapezoid"
+ "pentagon"
+ "hexagon"
+ "heptagon"
+ "octagon"
+ "decagon"
+ "dodecagon"
+ "star4"
+ "star5"
+ "star6"
+ "star7"
+ "star8"
+ "star10"
+ "star12"
+ "star16"
+ "star24"
+ "star32"
+ "roundRect"
+ "round1Rect"
+ "round2SameRect"
+ "round2DiagRect"
+ "snipRoundRect"
+ "snip1Rect"
+ "snip2SameRect"
+ "snip2DiagRect"
+ "plaque"
+ "ellipse"
+ "teardrop"
+ "homePlate"
+ "chevron"
+ "pieWedge"
+ "pie"
+ "blockArc"
+ "donut"
+ "noSmoking"
+ "rightArrow"
+ "leftArrow"
+ "upArrow"
+ "downArrow"
+ "stripedRightArrow"
+ "notchedRightArrow"
+ "bentUpArrow"
+ "leftRightArrow"
+ "upDownArrow"
+ "leftUpArrow"
+ "leftRightUpArrow"
+ "quadArrow"
+ "leftArrowCallout"
+ "rightArrowCallout"
+ "upArrowCallout"
+ "downArrowCallout"
+ "leftRightArrowCallout"
+ "upDownArrowCallout"
+ "quadArrowCallout"
+ "bentArrow"
+ "uturnArrow"
+ "circularArrow"
+ "leftCircularArrow"
+ "leftRightCircularArrow"
+ "curvedRightArrow"
+ "curvedLeftArrow"
+ "curvedUpArrow"
+ "curvedDownArrow"
+ "swooshArrow"
+ "cube"
+ "can"
+ "lightningBolt"
+ "heart"
+ "sun"
+ "moon"
+ "smileyFace"
+ "irregularSeal1"
+ "irregularSeal2"
+ "foldedCorner"
+ "bevel"
+ "frame"
+ "halfFrame"
+ "corner"
+ "diagStripe"
+ "chord"
+ "arc"
+ "leftBracket"
+ "rightBracket"
+ "leftBrace"
+ "rightBrace"
+ "bracketPair"
+ "bracePair"
+ "straightConnector1"
+ "bentConnector2"
+ "bentConnector3"
+ "bentConnector4"
+ "bentConnector5"
+ "curvedConnector2"
+ "curvedConnector3"
+ "curvedConnector4"
+ "curvedConnector5"
+ "callout1"
+ "callout2"
+ "callout3"
+ "accentCallout1"
+ "accentCallout2"
+ "accentCallout3"
+ "borderCallout1"
+ "borderCallout2"
+ "borderCallout3"
+ "accentBorderCallout1"
+ "accentBorderCallout2"
+ "accentBorderCallout3"
+ "wedgeRectCallout"
+ "wedgeRoundRectCallout"
+ "wedgeEllipseCallout"
+ "cloudCallout"
+ "cloud"
+ "ribbon"
+ "ribbon2"
+ "ellipseRibbon"
+ "ellipseRibbon2"
+ "leftRightRibbon"
+ "verticalScroll"
+ "horizontalScroll"
+ "wave"
+ "doubleWave"
+ "plus"
+ "flowChartProcess"
+ "flowChartDecision"
+ "flowChartInputOutput"
+ "flowChartPredefinedProcess"
+ "flowChartInternalStorage"
+ "flowChartDocument"
+ "flowChartMultidocument"
+ "flowChartTerminator"
+ "flowChartPreparation"
+ "flowChartManualInput"
+ "flowChartManualOperation"
+ "flowChartConnector"
+ "flowChartPunchedCard"
+ "flowChartPunchedTape"
+ "flowChartSummingJunction"
+ "flowChartOr"
+ "flowChartCollate"
+ "flowChartSort"
+ "flowChartExtract"
+ "flowChartMerge"
+ "flowChartOfflineStorage"
+ "flowChartOnlineStorage"
+ "flowChartMagneticTape"
+ "flowChartMagneticDisk"
+ "flowChartMagneticDrum"
+ "flowChartDisplay"
+ "flowChartDelay"
+ "flowChartAlternateProcess"
+ "flowChartOffpageConnector"
+ "actionButtonBlank"
+ "actionButtonHome"
+ "actionButtonHelp"
+ "actionButtonInformation"
+ "actionButtonForwardNext"
+ "actionButtonBackPrevious"
+ "actionButtonEnd"
+ "actionButtonBeginning"
+ "actionButtonReturn"
+ "actionButtonDocument"
+ "actionButtonSound"
+ "actionButtonMovie"
+ "gear6"
+ "gear9"
+ "funnel"
+ "mathPlus"
+ "mathMinus"
+ "mathMultiply"
+ "mathDivide"
+ "mathEqual"
+ "mathNotEqual"
+ "cornerTabs"
+ "squareTabs"
+ "plaqueTabs"
+ "chartX"
+ "chartStar"
+ "chartPlus"
+ '''
+
+ def __init__(self, coordinates=((0,0), (1,1)), text=None, scheme="accent1"):
+
+ self.coordinates = coordinates # in axis unit
+ self.text = text
+ self.scheme = scheme
+ self.style = Shape.RECT
+ self._border_width = 3175 # in EMU
+ self._border_color = Color.BLACK[2:] #"F3B3C5"
+ self._color = Color.WHITE[2:]
+ self._text_color = Color.BLACK[2:]
+
+ def _get_border_color(self):
+ return self._border_color
+
+ def _set_border_color(self, color):
+ self._border_color = short_color(color)
+
+ border_color = property(_get_border_color, _set_border_color)
+
+ def _get_color(self):
+ return self._color
+
+ def _set_color(self, color):
+ self._color = short_color(color)
+
+ color = property(_get_color, _set_color)
+
+ def _get_text_color(self):
+ return self._text_color
+
+ def _set_text_color(self, color):
+ self._text_color = short_color(color)
+
+ text_color = property(_get_text_color, _set_text_color)
+
+ def _get_border_width(self):
+
+ return EMU_to_pixels(self._border_width)
+
+ def _set_border_width(self, w):
+
+ self._border_width = pixels_to_EMU(w)
+ print(self._border_width)
+
+ border_width = property(_get_border_width, _set_border_width)
+
+ def get_coordinates(self):
+ """ return shape coordinates in percentages (left, top, right, bottom) """
+
+ (x1, y1), (x2, y2) = self.coordinates
+
+ drawing_width = pixels_to_EMU(self._chart.drawing.width)
+ drawing_height = pixels_to_EMU(self._chart.drawing.height)
+ plot_width = drawing_width * self._chart.width
+ plot_height = drawing_height * self._chart.height
+
+ margin_left = self._chart._get_margin_left() * drawing_width
+ xunit = plot_width / self._chart.get_x_units()
+
+ margin_top = self._chart._get_margin_top() * drawing_height
+ yunit = self._chart.get_y_units()
+
+ x_start = (margin_left + (float(x1) * xunit)) / drawing_width
+ y_start = (margin_top + plot_height - (float(y1) * yunit)) / drawing_height
+
+ x_end = (margin_left + (float(x2) * xunit)) / drawing_width
+ y_end = (margin_top + plot_height - (float(y2) * yunit)) / drawing_height
+
+ def _norm_pct(pct):
+ """ force shapes to appear by truncating too large sizes """
+ if pct>1: pct = 1
+ elif pct<0: pct = 0
+ return pct
+
+ # allow user to specify y's in whatever order
+ # excel expect y_end to be lower
+ if y_end < y_start:
+ y_end, y_start = y_start, y_end
+
+ return (_norm_pct(x_start), _norm_pct(y_start),
+ _norm_pct(x_end), _norm_pct(y_end))
+ \ No newline at end of file
diff --git a/tablib/packages/openpyxl3/namedrange.py b/tablib/packages/openpyxl3/namedrange.py
new file mode 100644
index 0000000..85b08a8
--- /dev/null
+++ b/tablib/packages/openpyxl3/namedrange.py
@@ -0,0 +1,68 @@
+# file openpyxl/namedrange.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Track named groups of cells in a worksheet"""
+
+# Python stdlib imports
+import re
+
+# package imports
+from .shared.exc import NamedRangeException
+
+# constants
+NAMED_RANGE_RE = re.compile("'?([^']*)'?!((\$([A-Za-z]+))?\$([0-9]+)(:(\$([A-Za-z]+))?(\$([0-9]+)))?)$")
+
+class NamedRange(object):
+ """A named group of cells"""
+ __slots__ = ('name', 'destinations', 'local_only')
+
+ def __init__(self, name, destinations):
+ self.name = name
+ self.destinations = destinations
+ self.local_only = False
+
+ def __str__(self):
+ return ','.join(['%s!%s' % (sheet, name) for sheet, name in self.destinations])
+
+ def __repr__(self):
+
+ return '<%s "%s">' % (self.__class__.__name__, str(self))
+
+
+def split_named_range(range_string):
+ """Separate a named range into its component parts"""
+
+ destinations = []
+
+ for range_string in range_string.split(','):
+
+ match = NAMED_RANGE_RE.match(range_string)
+ if not match:
+ raise NamedRangeException('Invalid named range string: "%s"' % range_string)
+ else:
+ sheet_name, xlrange = match.groups()[:2]
+ destinations.append((sheet_name, xlrange))
+
+ return destinations
diff --git a/tablib/packages/openpyxl3/reader/__init__.py b/tablib/packages/openpyxl3/reader/__init__.py
new file mode 100644
index 0000000..76f10f8
--- /dev/null
+++ b/tablib/packages/openpyxl3/reader/__init__.py
@@ -0,0 +1,33 @@
+# file openpyxl/reader/__init__.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Imports for the openpyxl.reader namespace."""
+
+# package imports
+from . import excel
+from . import strings
+from . import style
+from . import workbook
+from . import worksheet
diff --git a/tablib/packages/openpyxl3/reader/excel.py b/tablib/packages/openpyxl3/reader/excel.py
new file mode 100644
index 0000000..3fee695
--- /dev/null
+++ b/tablib/packages/openpyxl3/reader/excel.py
@@ -0,0 +1,117 @@
+# file openpyxl/reader/excel.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Read an xlsx file into Python"""
+
+# Python stdlib imports
+from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
+
+# package imports
+from ..shared.exc import OpenModeError, InvalidFileException
+from ..shared.ooxml import ARC_SHARED_STRINGS, ARC_CORE, ARC_APP, \
+ ARC_WORKBOOK, PACKAGE_WORKSHEETS, ARC_STYLE
+from ..workbook import Workbook
+from .strings import read_string_table
+from .style import read_style_table
+from .workbook import read_sheets_titles, read_named_ranges, \
+ read_properties_core, get_sheet_ids
+from .worksheet import read_worksheet
+from .iter_worksheet import unpack_worksheet
+
+def load_workbook(filename, use_iterators = False):
+ """Open the given filename and return the workbook
+
+ :param filename: the path to open
+ :type filename: string
+
+ :param use_iterators: use lazy load for cells
+ :type use_iterators: bool
+
+ :rtype: :class:`..workbook.Workbook`
+
+ .. note::
+
+ When using lazy load, all worksheets will be :class:`.iter_worksheet.IterableWorksheet`
+ and the returned workbook will be read-only.
+
+ """
+
+ if isinstance(filename, file):
+ # fileobject must have been opened with 'rb' flag
+ # it is required by zipfile
+ if 'b' not in filename.mode:
+ raise OpenModeError("File-object must be opened in binary mode")
+
+ try:
+ archive = ZipFile(filename, 'r', ZIP_DEFLATED)
+ except (BadZipfile, RuntimeError, IOError, ValueError) as e:
+ raise InvalidFileException(str(e))
+ wb = Workbook()
+
+ if use_iterators:
+ wb._set_optimized_read()
+
+ try:
+ _load_workbook(wb, archive, filename, use_iterators)
+ except KeyError as e:
+ raise InvalidFileException(str(e))
+ except Exception as e:
+ raise e
+ finally:
+ archive.close()
+ return wb
+
+def _load_workbook(wb, archive, filename, use_iterators):
+
+ valid_files = archive.namelist()
+
+ # get workbook-level information
+ wb.properties = read_properties_core(archive.read(ARC_CORE))
+ try:
+ string_table = read_string_table(archive.read(ARC_SHARED_STRINGS))
+ except KeyError:
+ string_table = {}
+ style_table = read_style_table(archive.read(ARC_STYLE))
+
+ # get worksheets
+ wb.worksheets = [] # remove preset worksheet
+ sheet_names = read_sheets_titles(archive.read(ARC_APP))
+ for i, sheet_name in enumerate(sheet_names):
+
+ sheet_codename = 'sheet%d.xml' % (i + 1)
+ worksheet_path = '%s/%s' % (PACKAGE_WORKSHEETS, sheet_codename)
+
+ if not worksheet_path in valid_files:
+ continue
+
+ if not use_iterators:
+ new_ws = read_worksheet(archive.read(worksheet_path), wb, sheet_name, string_table, style_table)
+ else:
+ xml_source = unpack_worksheet(archive, worksheet_path)
+ new_ws = read_worksheet(xml_source, wb, sheet_name, string_table, style_table, filename, sheet_codename)
+ #new_ws = read_worksheet(archive.read(worksheet_path), wb, sheet_name, string_table, style_table, filename, sheet_codename)
+ wb.add_sheet(new_ws, index = i)
+
+ wb._named_ranges = read_named_ranges(archive.read(ARC_WORKBOOK), wb)
diff --git a/tablib/packages/openpyxl3/reader/iter_worksheet.py b/tablib/packages/openpyxl3/reader/iter_worksheet.py
new file mode 100644
index 0000000..670e6b1
--- /dev/null
+++ b/tablib/packages/openpyxl3/reader/iter_worksheet.py
@@ -0,0 +1,343 @@
+# file openpyxl/reader/iter_worksheet.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+""" Iterators-based worksheet reader
+*Still very raw*
+"""
+
+from io import StringIO
+import warnings
+import operator
+from functools import partial
+from itertools import groupby
+from ..worksheet import Worksheet
+from ..cell import coordinate_from_string, get_column_letter, Cell
+from .excel import get_sheet_ids
+from .strings import read_string_table
+from .style import read_style_table, NumberFormat
+from ..shared.date_time import SharedDate
+from .worksheet import read_dimension
+from ..shared.ooxml import (MIN_COLUMN, MAX_COLUMN, PACKAGE_WORKSHEETS,
+ MAX_ROW, MIN_ROW, ARC_SHARED_STRINGS, ARC_APP, ARC_STYLE)
+from xml.etree.cElementTree import iterparse
+from zipfile import ZipFile
+from .. import cell
+import re
+import tempfile
+import zlib
+import zipfile
+import struct
+
+TYPE_NULL = Cell.TYPE_NULL
+MISSING_VALUE = None
+
+RE_COORDINATE = re.compile('^([A-Z]+)([0-9]+)$')
+
+SHARED_DATE = SharedDate()
+
+_COL_CONVERSION_CACHE = dict((get_column_letter(i), i) for i in range(1, 18279))
+def column_index_from_string(str_col, _col_conversion_cache=_COL_CONVERSION_CACHE):
+ # we use a function argument to get indexed name lookup
+ return _col_conversion_cache[str_col]
+del _COL_CONVERSION_CACHE
+
+RAW_ATTRIBUTES = ['row', 'column', 'coordinate', 'internal_value', 'data_type', 'style_id', 'number_format']
+
+try:
+ from collections import namedtuple
+ BaseRawCell = namedtuple('RawCell', RAW_ATTRIBUTES)
+except ImportError:
+
+ warnings.warn("""Unable to import 'namedtuple' module, this may cause memory issues when using optimized reader. Please upgrade your Python installation to 2.6+""")
+
+ class BaseRawCell(object):
+
+ def __init__(self, *args):
+ assert len(args)==len(RAW_ATTRIBUTES)
+
+ for attr, val in zip(RAW_ATTRIBUTES, args):
+ setattr(self, attr, val)
+
+ def _replace(self, **kwargs):
+
+ self.__dict__.update(kwargs)
+
+ return self
+
+
+class RawCell(BaseRawCell):
+ """Optimized version of the :class:`..cell.Cell`, using named tuples.
+
+ Useful attributes are:
+
+ * row
+ * column
+ * coordinate
+ * internal_value
+
+ You can also access if needed:
+
+ * data_type
+ * number_format
+
+ """
+
+ @property
+ def is_date(self):
+ res = (self.data_type == Cell.TYPE_NUMERIC
+ and self.number_format is not None
+ and ('d' in self.number_format
+ or 'm' in self.number_format
+ or 'y' in self.number_format
+ or 'h' in self.number_format
+ or 's' in self.number_format
+ ))
+
+ return res
+
+def iter_rows(workbook_name, sheet_name, xml_source, range_string = '', row_offset = 0, column_offset = 0):
+
+ archive = get_archive_file(workbook_name)
+
+ source = xml_source
+
+ if range_string:
+ min_col, min_row, max_col, max_row = get_range_boundaries(range_string, row_offset, column_offset)
+ else:
+ min_col, min_row, max_col, max_row = read_dimension(xml_source = source)
+ min_col = column_index_from_string(min_col)
+ max_col = column_index_from_string(max_col) + 1
+ max_row += 6
+
+ try:
+ string_table = read_string_table(archive.read(ARC_SHARED_STRINGS))
+ except KeyError:
+ string_table = {}
+
+ style_table = read_style_table(archive.read(ARC_STYLE))
+
+ source.seek(0)
+ p = iterparse(source)
+
+ return get_squared_range(p, min_col, min_row, max_col, max_row, string_table, style_table)
+
+
+def get_rows(p, min_column = MIN_COLUMN, min_row = MIN_ROW, max_column = MAX_COLUMN, max_row = MAX_ROW):
+
+ return groupby(get_cells(p, min_row, min_column, max_row, max_column), operator.attrgetter('row'))
+
+def get_cells(p, min_row, min_col, max_row, max_col, _re_coordinate=RE_COORDINATE):
+
+ for _event, element in p:
+
+ if element.tag == '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}c':
+ coord = element.get('r')
+ column_str, row = _re_coordinate.match(coord).groups()
+
+ row = int(row)
+ column = column_index_from_string(column_str)
+
+ if min_col <= column <= max_col and min_row <= row <= max_row:
+ data_type = element.get('t', 'n')
+ style_id = element.get('s')
+ value = element.findtext('{http://schemas.openxmlformats.org/spreadsheetml/2006/main}v')
+ yield RawCell(row, column_str, coord, value, data_type, style_id, None)
+
+ if element.tag == '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}v':
+ continue
+ element.clear()
+
+
+
+def get_range_boundaries(range_string, row = 0, column = 0):
+
+ if ':' in range_string:
+ min_range, max_range = range_string.split(':')
+ min_col, min_row = coordinate_from_string(min_range)
+ max_col, max_row = coordinate_from_string(max_range)
+
+ min_col = column_index_from_string(min_col) + column
+ max_col = column_index_from_string(max_col) + column
+ min_row += row
+ max_row += row
+
+ else:
+ min_col, min_row = coordinate_from_string(range_string)
+ min_col = column_index_from_string(min_col)
+ max_col = min_col + 1
+ max_row = min_row
+
+ return (min_col, min_row, max_col, max_row)
+
+def get_archive_file(archive_name):
+
+ return ZipFile(archive_name, 'r')
+
+def get_xml_source(archive_file, sheet_name):
+
+ return archive_file.read('%s/%s' % (PACKAGE_WORKSHEETS, sheet_name))
+
+def get_missing_cells(row, columns):
+
+ return dict([(column, RawCell(row, column, '%s%s' % (column, row), MISSING_VALUE, TYPE_NULL, None, None)) for column in columns])
+
+def get_squared_range(p, min_col, min_row, max_col, max_row, string_table, style_table):
+
+ expected_columns = [get_column_letter(ci) for ci in range(min_col, max_col)]
+
+ current_row = min_row
+ for row, cells in get_rows(p, min_row = min_row, max_row = max_row, min_column = min_col, max_column = max_col):
+ full_row = []
+ if current_row < row:
+
+ for gap_row in range(current_row, row):
+
+ dummy_cells = get_missing_cells(gap_row, expected_columns)
+
+ yield tuple([dummy_cells[column] for column in expected_columns])
+
+ current_row = row
+
+ temp_cells = list(cells)
+
+ retrieved_columns = dict([(c.column, c) for c in temp_cells])
+
+ missing_columns = list(set(expected_columns) - set(retrieved_columns.keys()))
+
+ replacement_columns = get_missing_cells(row, missing_columns)
+
+ for column in expected_columns:
+
+ if column in retrieved_columns:
+ cell = retrieved_columns[column]
+
+ if cell.style_id is not None:
+ style = style_table[int(cell.style_id)]
+ cell = cell._replace(number_format = style.number_format.format_code) #pylint: disable-msg=W0212
+ if cell.internal_value is not None:
+ if cell.data_type == Cell.TYPE_STRING:
+ cell = cell._replace(internal_value = string_table[int(cell.internal_value)]) #pylint: disable-msg=W0212
+ elif cell.data_type == Cell.TYPE_BOOL:
+ cell = cell._replace(internal_value = cell.internal_value == 'True')
+ elif cell.is_date:
+ cell = cell._replace(internal_value = SHARED_DATE.from_julian(float(cell.internal_value)))
+ elif cell.data_type == Cell.TYPE_NUMERIC:
+ cell = cell._replace(internal_value = float(cell.internal_value))
+ full_row.append(cell)
+
+ else:
+ full_row.append(replacement_columns[column])
+
+ current_row = row + 1
+
+ yield tuple(full_row)
+
+#------------------------------------------------------------------------------
+
+class IterableWorksheet(Worksheet):
+
+ def __init__(self, parent_workbook, title, workbook_name,
+ sheet_codename, xml_source):
+
+ Worksheet.__init__(self, parent_workbook, title)
+ self._workbook_name = workbook_name
+ self._sheet_codename = sheet_codename
+ self._xml_source = xml_source
+
+ def iter_rows(self, range_string = '', row_offset = 0, column_offset = 0):
+ """ Returns a squared range based on the `range_string` parameter,
+ using generators.
+
+ :param range_string: range of cells (e.g. 'A1:C4')
+ :type range_string: string
+
+ :param row: row index of the cell (e.g. 4)
+ :type row: int
+
+ :param column: column index of the cell (e.g. 3)
+ :type column: int
+
+ :rtype: generator
+
+ """
+
+ return iter_rows(workbook_name = self._workbook_name,
+ sheet_name = self._sheet_codename,
+ xml_source = self._xml_source,
+ range_string = range_string,
+ row_offset = row_offset,
+ column_offset = column_offset)
+
+ def cell(self, *args, **kwargs):
+
+ raise NotImplementedError("use 'iter_rows()' instead")
+
+ def range(self, *args, **kwargs):
+
+ raise NotImplementedError("use 'iter_rows()' instead")
+
+def unpack_worksheet(archive, filename):
+
+ temp_file = tempfile.TemporaryFile(mode='r+', prefix='openpyxl.', suffix='.unpack.temp')
+
+ zinfo = archive.getinfo(filename)
+
+ if zinfo.compress_type == zipfile.ZIP_STORED:
+ decoder = None
+ elif zinfo.compress_type == zipfile.ZIP_DEFLATED:
+ decoder = zlib.decompressobj(-zlib.MAX_WBITS)
+ else:
+ raise zipfile.BadZipFile("Unrecognized compression method")
+
+ archive.fp.seek(_get_file_offset(archive, zinfo))
+ bytes_to_read = zinfo.compress_size
+
+ while True:
+ buff = archive.fp.read(min(bytes_to_read, 102400))
+ if not buff:
+ break
+ bytes_to_read -= len(buff)
+ if decoder:
+ buff = decoder.decompress(buff)
+ temp_file.write(buff)
+
+ if decoder:
+ temp_file.write(decoder.decompress('Z'))
+
+ return temp_file
+
+def _get_file_offset(archive, zinfo):
+
+ try:
+ return zinfo.file_offset
+ except AttributeError:
+ # From http://stackoverflow.com/questions/3781261/how-to-simulate-zipfile-open-in-python-2-5
+
+ # Seek over the fixed size fields to the "file name length" field in
+ # the file header (26 bytes). Unpack this and the "extra field length"
+ # field ourselves as info.extra doesn't seem to be the correct length.
+ archive.fp.seek(zinfo.header_offset + 26)
+ file_name_len, extra_len = struct.unpack("<HH", archive.fp.read(4))
+ return zinfo.header_offset + 30 + file_name_len + extra_len
diff --git a/tablib/packages/openpyxl3/reader/strings.py b/tablib/packages/openpyxl3/reader/strings.py
new file mode 100644
index 0000000..d3a897a
--- /dev/null
+++ b/tablib/packages/openpyxl3/reader/strings.py
@@ -0,0 +1,64 @@
+# file openpyxl/reader/strings.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Read the shared strings table."""
+
+# package imports
+from ..shared.xmltools import fromstring, QName
+from ..shared.ooxml import NAMESPACES
+
+
+def read_string_table(xml_source):
+ """Read in all shared strings in the table"""
+ table = {}
+ xmlns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
+ root = fromstring(text=xml_source)
+ string_index_nodes = root.findall(QName(xmlns, 'si').text)
+ for index, string_index_node in enumerate(string_index_nodes):
+ table[index] = get_string(xmlns, string_index_node)
+ return table
+
+
+def get_string(xmlns, string_index_node):
+ """Read the contents of a specific string index"""
+ rich_nodes = string_index_node.findall(QName(xmlns, 'r').text)
+ if rich_nodes:
+ reconstructed_text = []
+ for rich_node in rich_nodes:
+ partial_text = get_text(xmlns, rich_node)
+ reconstructed_text.append(partial_text)
+ return ''.join(reconstructed_text)
+ else:
+ return get_text(xmlns, string_index_node)
+
+
+def get_text(xmlns, rich_node):
+ """Read rich text, discarding formatting if not disallowed"""
+ text_node = rich_node.find(QName(xmlns, 't').text)
+ partial_text = text_node.text or ''
+
+ if text_node.get(QName(NAMESPACES['xml'], 'space').text) != 'preserve':
+ partial_text = partial_text.strip()
+ return str(partial_text)
diff --git a/tablib/packages/openpyxl3/reader/style.py b/tablib/packages/openpyxl3/reader/style.py
new file mode 100644
index 0000000..f773070
--- /dev/null
+++ b/tablib/packages/openpyxl3/reader/style.py
@@ -0,0 +1,69 @@
+# file openpyxl/reader/style.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Read shared style definitions"""
+
+# package imports
+from ..shared.xmltools import fromstring, QName
+from ..shared.exc import MissingNumberFormat
+from ..style import Style, NumberFormat
+
+
+def read_style_table(xml_source):
+ """Read styles from the shared style table"""
+ table = {}
+ xmlns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
+ root = fromstring(xml_source)
+ custom_num_formats = parse_custom_num_formats(root, xmlns)
+ builtin_formats = NumberFormat._BUILTIN_FORMATS
+ cell_xfs = root.find(QName(xmlns, 'cellXfs').text)
+ cell_xfs_nodes = cell_xfs.findall(QName(xmlns, 'xf').text)
+ for index, cell_xfs_node in enumerate(cell_xfs_nodes):
+ new_style = Style()
+ number_format_id = int(cell_xfs_node.get('numFmtId'))
+ if number_format_id < 164:
+ new_style.number_format.format_code = \
+ builtin_formats.get(number_format_id, 'General')
+ else:
+
+ if number_format_id in custom_num_formats:
+ new_style.number_format.format_code = \
+ custom_num_formats[number_format_id]
+ else:
+ raise MissingNumberFormat('%s' % number_format_id)
+ table[index] = new_style
+ return table
+
+
+def parse_custom_num_formats(root, xmlns):
+ """Read in custom numeric formatting rules from the shared style table"""
+ custom_formats = {}
+ num_fmts = root.find(QName(xmlns, 'numFmts').text)
+ if num_fmts is not None:
+ num_fmt_nodes = num_fmts.findall(QName(xmlns, 'numFmt').text)
+ for num_fmt_node in num_fmt_nodes:
+ custom_formats[int(num_fmt_node.get('numFmtId'))] = \
+ num_fmt_node.get('formatCode')
+ return custom_formats
diff --git a/tablib/packages/openpyxl3/reader/workbook.py b/tablib/packages/openpyxl3/reader/workbook.py
new file mode 100644
index 0000000..d9bc161
--- /dev/null
+++ b/tablib/packages/openpyxl3/reader/workbook.py
@@ -0,0 +1,156 @@
+# file openpyxl/reader/workbook.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Read in global settings to be maintained by the workbook object."""
+
+# package imports
+from ..shared.xmltools import fromstring, QName
+from ..shared.ooxml import NAMESPACES
+from ..workbook import DocumentProperties
+from ..shared.date_time import W3CDTF_to_datetime
+from ..namedrange import NamedRange, split_named_range
+
+import datetime
+
+# constants
+BUGGY_NAMED_RANGES = ['NA()', '#REF!']
+DISCARDED_RANGES = ['Excel_BuiltIn', 'Print_Area']
+
+def get_sheet_ids(xml_source):
+
+ sheet_names = read_sheets_titles(xml_source)
+
+ return dict((sheet, 'sheet%d.xml' % (i + 1)) for i, sheet in enumerate(sheet_names))
+
+
+def read_properties_core(xml_source):
+ """Read assorted file properties."""
+ properties = DocumentProperties()
+ root = fromstring(xml_source)
+ creator_node = root.find(QName(NAMESPACES['dc'], 'creator').text)
+ if creator_node is not None:
+ properties.creator = creator_node.text
+ else:
+ properties.creator = ''
+ last_modified_by_node = root.find(
+ QName(NAMESPACES['cp'], 'lastModifiedBy').text)
+ if last_modified_by_node is not None:
+ properties.last_modified_by = last_modified_by_node.text
+ else:
+ properties.last_modified_by = ''
+
+ created_node = root.find(QName(NAMESPACES['dcterms'], 'created').text)
+ if created_node is not None:
+ properties.created = W3CDTF_to_datetime(created_node.text)
+ else:
+ properties.created = datetime.datetime.now()
+
+ modified_node = root.find(QName(NAMESPACES['dcterms'], 'modified').text)
+ if modified_node is not None:
+ properties.modified = W3CDTF_to_datetime(modified_node.text)
+ else:
+ properties.modified = properties.created
+
+ return properties
+
+
+def get_number_of_parts(xml_source):
+ """Get a list of contents of the workbook."""
+ parts_size = {}
+ parts_names = []
+ root = fromstring(xml_source)
+ heading_pairs = root.find(QName('http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
+ 'HeadingPairs').text)
+ vector = heading_pairs.find(QName(NAMESPACES['vt'], 'vector').text)
+ children = vector.getchildren()
+ for child_id in range(0, len(children), 2):
+ part_name = children[child_id].find(QName(NAMESPACES['vt'],
+ 'lpstr').text).text
+ if not part_name in parts_names:
+ parts_names.append(part_name)
+ part_size = int(children[child_id + 1].find(QName(
+ NAMESPACES['vt'], 'i4').text).text)
+ parts_size[part_name] = part_size
+ return parts_size, parts_names
+
+
+def read_sheets_titles(xml_source):
+ """Read titles for all sheets."""
+ root = fromstring(xml_source)
+ titles_root = root.find(QName('http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
+ 'TitlesOfParts').text)
+ vector = titles_root.find(QName(NAMESPACES['vt'], 'vector').text)
+ parts, names = get_number_of_parts(xml_source)
+
+ # we can't assume 'Worksheets' to be written in english,
+ # but it's always the first item of the parts list (see bug #22)
+ size = parts[names[0]]
+ children = [c.text for c in vector.getchildren()]
+ return children[:size]
+
+
+def read_named_ranges(xml_source, workbook):
+ """Read named ranges, excluding poorly defined ranges."""
+ named_ranges = []
+ root = fromstring(xml_source)
+ names_root = root.find(QName('http://schemas.openxmlformats.org/spreadsheetml/2006/main',
+ 'definedNames').text)
+ if names_root is not None:
+
+ for name_node in names_root.getchildren():
+ range_name = name_node.get('name')
+
+ if name_node.get("hidden", '0') == '1':
+ continue
+
+ valid = True
+
+ for discarded_range in DISCARDED_RANGES:
+ if discarded_range in range_name:
+ valid = False
+
+ for bad_range in BUGGY_NAMED_RANGES:
+ if bad_range in name_node.text:
+ valid = False
+
+ if valid:
+ destinations = split_named_range(name_node.text)
+
+ new_destinations = []
+ for worksheet, cells_range in destinations:
+
+ # it can happen that a valid named range references
+ # a missing worksheet, when Excel didn't properly maintain
+ # the named range list
+ #
+ # we just ignore them here
+ worksheet = workbook.get_sheet_by_name(worksheet)
+ if worksheet:
+ new_destinations.append((worksheet, cells_range))
+
+ named_range = NamedRange(range_name, new_destinations)
+ named_ranges.append(named_range)
+
+ return named_ranges
diff --git a/tablib/packages/openpyxl3/reader/worksheet.py b/tablib/packages/openpyxl3/reader/worksheet.py
new file mode 100644
index 0000000..c1c084c
--- /dev/null
+++ b/tablib/packages/openpyxl3/reader/worksheet.py
@@ -0,0 +1,117 @@
+# file openpyxl/reader/worksheet.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Reader for a single worksheet."""
+
+# Python stdlib imports
+try:
+ from xml.etree.cElementTree import iterparse
+except ImportError:
+ from xml.etree.ElementTree import iterparse
+
+from io import StringIO
+
+# package imports
+from ..cell import Cell, coordinate_from_string
+from ..worksheet import Worksheet
+
+def _get_xml_iter(xml_source):
+
+ if not hasattr(xml_source, 'name'):
+ return StringIO(xml_source)
+ else:
+ xml_source.seek(0)
+ return xml_source
+
+def read_dimension(xml_source):
+
+ source = _get_xml_iter(xml_source)
+
+ it = iterparse(source)
+
+ for event, element in it:
+
+ if element.tag == '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}dimension':
+ ref = element.get('ref')
+
+ if ':' in ref:
+ min_range, max_range = ref.split(':')
+ else:
+ min_range = max_range = ref
+
+ min_col, min_row = coordinate_from_string(min_range)
+ max_col, max_row = coordinate_from_string(max_range)
+
+ return min_col, min_row, max_col, max_row
+
+ else:
+ element.clear()
+
+ return None
+
+def filter_cells(xxx_todo_changeme):
+
+ (event, element) = xxx_todo_changeme
+ return element.tag == '{http://schemas.openxmlformats.org/spreadsheetml/2006/main}c'
+
+def fast_parse(ws, xml_source, string_table, style_table):
+
+ source = _get_xml_iter(xml_source)
+
+ it = iterparse(source)
+
+ for event, element in filter(filter_cells, it):
+
+ value = element.findtext('{http://schemas.openxmlformats.org/spreadsheetml/2006/main}v')
+
+ if value is not None:
+
+ coordinate = element.get('r')
+ data_type = element.get('t', 'n')
+ style_id = element.get('s')
+
+ if data_type == Cell.TYPE_STRING:
+ value = string_table.get(int(value))
+
+ ws.cell(coordinate).value = value
+
+ if style_id is not None:
+ ws._styles[coordinate] = style_table.get(int(style_id))
+
+ # to avoid memory exhaustion, clear the item after use
+ element.clear()
+
+from ..reader.iter_worksheet import IterableWorksheet
+
+def read_worksheet(xml_source, parent, preset_title, string_table,
+ style_table, workbook_name = None, sheet_codename = None):
+ """Read an xml worksheet"""
+ if workbook_name and sheet_codename:
+ ws = IterableWorksheet(parent, preset_title, workbook_name,
+ sheet_codename, xml_source)
+ else:
+ ws = Worksheet(parent, preset_title)
+ fast_parse(ws, xml_source, string_table, style_table)
+ return ws
diff --git a/tablib/packages/openpyxl3/shared/__init__.py b/tablib/packages/openpyxl3/shared/__init__.py
new file mode 100644
index 0000000..c19d52c
--- /dev/null
+++ b/tablib/packages/openpyxl3/shared/__init__.py
@@ -0,0 +1,33 @@
+# file openpyxl/shared/__init__.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Imports for the . namespace."""
+
+# package imports
+from . import date_time
+from . import exc
+from . import ooxml
+from . import password_hasher
+from . import xmltools
diff --git a/tablib/packages/openpyxl3/shared/date_time.py b/tablib/packages/openpyxl3/shared/date_time.py
new file mode 100644
index 0000000..8a58cd0
--- /dev/null
+++ b/tablib/packages/openpyxl3/shared/date_time.py
@@ -0,0 +1,154 @@
+# file openpyxl/shared/date_time.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Manage Excel date weirdness."""
+
+# Python stdlib imports
+
+from math import floor
+import calendar
+import datetime
+import time
+import re
+
+# constants
+W3CDTF_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+
+RE_W3CDTF = '(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(.(\d{2}))?Z'
+
+EPOCH = datetime.datetime.utcfromtimestamp(0)
+
+def datetime_to_W3CDTF(dt):
+ """Convert from a datetime to a timestamp string."""
+ return datetime.datetime.strftime(dt, W3CDTF_FORMAT)
+
+
+def W3CDTF_to_datetime(formatted_string):
+ """Convert from a timestamp string to a datetime object."""
+ match = re.match(RE_W3CDTF,formatted_string)
+ digits = list(map(int, match.groups()[:6]))
+ return datetime.datetime(*digits)
+
+
+class SharedDate(object):
+ """Date formatting utilities for Excel with shared state.
+
+ Excel has a two primary date tracking schemes:
+ Windows - Day 1 == 1900-01-01
+ Mac - Day 1 == 1904-01-01
+
+ SharedDate stores which system we are using and converts dates between
+ Python and Excel accordingly.
+
+ """
+ CALENDAR_WINDOWS_1900 = 1900
+ CALENDAR_MAC_1904 = 1904
+ datetime_object_type = 'DateTime'
+
+ def __init__(self):
+ self.excel_base_date = self.CALENDAR_WINDOWS_1900
+
+ def datetime_to_julian(self, date):
+ """Convert from python datetime to excel julian date representation."""
+
+ if isinstance(date, datetime.datetime):
+ return self.to_julian(date.year, date.month, date.day, \
+ hours=date.hour, minutes=date.minute, seconds=date.second)
+ elif isinstance(date, datetime.date):
+ return self.to_julian(date.year, date.month, date.day)
+
+ def to_julian(self, year, month, day, hours=0, minutes=0, seconds=0):
+ """Convert from Python date to Excel JD."""
+ # explicitly disallow bad years
+ # Excel 2000 treats JD=0 as 1/0/1900 (buggy, disallow)
+ # Excel 2000 treats JD=2958466 as a bad date (Y10K bug!)
+ if year < 1900 or year > 10000:
+ msg = 'Year not supported by Excel: %s' % year
+ raise ValueError(msg)
+ if self.excel_base_date == self.CALENDAR_WINDOWS_1900:
+ # Fudge factor for the erroneous fact that the year 1900 is
+ # treated as a Leap Year in MS Excel. This affects every date
+ # following 28th February 1900
+ if year == 1900 and month <= 2:
+ excel_1900_leap_year = False
+ else:
+ excel_1900_leap_year = True
+ excel_base_date = 2415020
+ else:
+ raise NotImplementedError('Mac dates are not yet supported.')
+ #excel_base_date = 2416481
+ #excel_1900_leap_year = False
+
+ # Julian base date adjustment
+ if month > 2:
+ month = month - 3
+ else:
+ month = month + 9
+ year -= 1
+
+ # Calculate the Julian Date, then subtract the Excel base date
+ # JD 2415020 = 31 - Dec - 1899 -> Excel Date of 0
+ century, decade = int(str(year)[:2]), int(str(year)[2:])
+ excel_date = floor(146097 * century / 4) + \
+ floor((1461 * decade) / 4) + floor((153 * month + 2) / 5) + \
+ day + 1721119 - excel_base_date
+ if excel_1900_leap_year:
+ excel_date += 1
+
+ # check to ensure that we exclude 2/29/1900 as a possible value
+ if self.excel_base_date == self.CALENDAR_WINDOWS_1900 \
+ and excel_date == 60:
+ msg = 'Error: Excel believes 1900 was a leap year'
+ raise ValueError(msg)
+ excel_time = ((hours * 3600) + (minutes * 60) + seconds) / 86400
+ return excel_date + excel_time
+
+ def from_julian(self, value=0):
+ """Convert from the Excel JD back to a date"""
+ if self.excel_base_date == self.CALENDAR_WINDOWS_1900:
+ excel_base_date = 25569
+ if value < 60:
+ excel_base_date -= 1
+ elif value == 60:
+ msg = 'Error: Excel believes 1900 was a leap year'
+ raise ValueError(msg)
+ else:
+ raise NotImplementedError('Mac dates are not yet supported.')
+ #excel_base_date = 24107
+
+ if value >= 1:
+ utc_days = value - excel_base_date
+
+ return EPOCH + datetime.timedelta(days=utc_days)
+
+ elif value >= 0:
+ hours = floor(value * 24)
+ mins = floor(value * 24 * 60) - floor(hours * 60)
+ secs = floor(value * 24 * 60 * 60) - floor(hours * 60 * 60) - \
+ floor(mins * 60)
+ return datetime.time(int(hours), int(mins), int(secs))
+ else:
+ msg = 'Negative dates (%s) are not supported' % value
+ raise ValueError(msg)
diff --git a/tablib/packages/openpyxl3/shared/exc.py b/tablib/packages/openpyxl3/shared/exc.py
new file mode 100644
index 0000000..94a3e2c
--- /dev/null
+++ b/tablib/packages/openpyxl3/shared/exc.py
@@ -0,0 +1,59 @@
+# file openpyxl/shared/exc.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Definitions for openpyxl shared exception classes."""
+
+
+class CellCoordinatesException(Exception):
+ """Error for converting between numeric and A1-style cell references."""
+
+class ColumnStringIndexException(Exception):
+ """Error for bad column names in A1-style cell references."""
+
+class DataTypeException(Exception):
+ """Error for any data type inconsistencies."""
+
+class NamedRangeException(Exception):
+ """Error for badly formatted named ranges."""
+
+class SheetTitleException(Exception):
+ """Error for bad sheet names."""
+
+class InsufficientCoordinatesException(Exception):
+ """Error for partially specified cell coordinates."""
+
+class OpenModeError(Exception):
+ """Error for fileobj opened in non-binary mode."""
+
+class InvalidFileException(Exception):
+ """Error for trying to open a non-ooxml file."""
+
+class ReadOnlyWorkbookException(Exception):
+ """Error for trying to modify a read-only workbook"""
+
+class MissingNumberFormat(Exception):
+ """Error when a referenced number format is not in the stylesheet"""
+
+
diff --git a/tablib/packages/openpyxl3/shared/ooxml.py b/tablib/packages/openpyxl3/shared/ooxml.py
new file mode 100644
index 0000000..979b172
--- /dev/null
+++ b/tablib/packages/openpyxl3/shared/ooxml.py
@@ -0,0 +1,60 @@
+# file openpyxl/shared/ooxml.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Constants for fixed paths in a file and xml namespace urls."""
+
+MIN_ROW = 0
+MIN_COLUMN = 0
+MAX_COLUMN = 16384
+MAX_ROW = 1048576
+
+# constants
+PACKAGE_PROPS = 'docProps'
+PACKAGE_XL = 'xl'
+PACKAGE_RELS = '_rels'
+PACKAGE_THEME = PACKAGE_XL + '/' + 'theme'
+PACKAGE_WORKSHEETS = PACKAGE_XL + '/' + 'worksheets'
+PACKAGE_DRAWINGS = PACKAGE_XL + '/' + 'drawings'
+PACKAGE_CHARTS = PACKAGE_XL + '/' + 'charts'
+
+ARC_CONTENT_TYPES = '[Content_Types].xml'
+ARC_ROOT_RELS = PACKAGE_RELS + '/.rels'
+ARC_WORKBOOK_RELS = PACKAGE_XL + '/' + PACKAGE_RELS + '/workbook.xml.rels'
+ARC_CORE = PACKAGE_PROPS + '/core.xml'
+ARC_APP = PACKAGE_PROPS + '/app.xml'
+ARC_WORKBOOK = PACKAGE_XL + '/workbook.xml'
+ARC_STYLE = PACKAGE_XL + '/styles.xml'
+ARC_THEME = PACKAGE_THEME + '/theme1.xml'
+ARC_SHARED_STRINGS = PACKAGE_XL + '/sharedStrings.xml'
+
+NAMESPACES = {
+ 'cp': 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties',
+ 'dc': 'http://purl.org/dc/elements/1.1/',
+ 'dcterms': 'http://purl.org/dc/terms/',
+ 'dcmitype': 'http://purl.org/dc/dcmitype/',
+ 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
+ 'vt': 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes',
+ 'xml': 'http://www.w3.org/XML/1998/namespace'
+}
diff --git a/tablib/packages/openpyxl3/shared/password_hasher.py b/tablib/packages/openpyxl3/shared/password_hasher.py
new file mode 100644
index 0000000..b5d0dd0
--- /dev/null
+++ b/tablib/packages/openpyxl3/shared/password_hasher.py
@@ -0,0 +1,47 @@
+# file openpyxl/shared/password_hasher.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Basic password hashing."""
+
+
+def hash_password(plaintext_password=''):
+ """Create a password hash from a given string.
+
+ This method is based on the algorithm provided by
+ Daniel Rentz of OpenOffice and the PEAR package
+ Spreadsheet_Excel_Writer by Xavier Noguer <xnoguer@rezebra.com>.
+
+ """
+ password = 0x0000
+ i = 1
+ for char in plaintext_password:
+ value = ord(char) << i
+ rotated_bits = value >> 15
+ value &= 0x7fff
+ password ^= (value | rotated_bits)
+ i += 1
+ password ^= len(plaintext_password)
+ password ^= 0xCE4B
+ return str(hex(password)).upper()[2:]
diff --git a/tablib/packages/openpyxl3/shared/units.py b/tablib/packages/openpyxl3/shared/units.py
new file mode 100644
index 0000000..fba82d7
--- /dev/null
+++ b/tablib/packages/openpyxl3/shared/units.py
@@ -0,0 +1,67 @@
+# file openpyxl/shared/units.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+import math
+
+def pixels_to_EMU(value):
+ return int(round(value * 9525))
+
+def EMU_to_pixels(value):
+ if not value:
+ return 0
+ else:
+ return round(value / 9525.)
+
+def EMU_to_cm(value):
+ if not value:
+ return 0
+ else:
+ return (EMU_to_pixels(value) * 2.57 / 96)
+
+def pixels_to_points(value):
+ return value * 0.67777777
+
+def points_to_pixels(value):
+ if not value:
+ return 0
+ else:
+ return int(math.ceil(value * 1.333333333))
+
+def degrees_to_angle(value):
+ return int(round(value * 60000))
+
+def angle_to_degrees(value):
+ if not value:
+ return 0
+ else:
+ return round(value / 60000.)
+
+def short_color(color):
+ """ format a color to its short size """
+
+ if len(color) > 6:
+ return color[2:]
+ else:
+ return color
diff --git a/tablib/packages/openpyxl3/shared/xmltools.py b/tablib/packages/openpyxl3/shared/xmltools.py
new file mode 100644
index 0000000..3c0be4c
--- /dev/null
+++ b/tablib/packages/openpyxl3/shared/xmltools.py
@@ -0,0 +1,96 @@
+# file openpyxl/shared/xmltools.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Shared xml tools.
+
+Shortcut functions taken from:
+ http://lethain.com/entry/2009/jan/22/handling-very-large-csv-and-xml-files-in-python/
+
+"""
+
+# Python stdlib imports
+from xml.sax.xmlreader import AttributesNSImpl
+from xml.sax.saxutils import XMLGenerator
+try:
+ from xml.etree.ElementTree import ElementTree, Element, SubElement, \
+ QName, fromstring, tostring
+except ImportError:
+ from cElementTree import ElementTree, Element, SubElement, \
+ QName, fromstring, tostring
+
+# package imports
+from .. import __name__ as prefix
+
+
+def get_document_content(xml_node):
+ """Print nicely formatted xml to a string."""
+ pretty_indent(xml_node)
+ return tostring(xml_node, 'utf-8')
+
+
+def pretty_indent(elem, level=0):
+ """Format xml with nice indents and line breaks."""
+ i = "\n" + level * " "
+ if len(elem):
+ if not elem.text or not elem.text.strip():
+ elem.text = i + " "
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ for elem in elem:
+ pretty_indent(elem, level + 1)
+ if not elem.tail or not elem.tail.strip():
+ elem.tail = i
+ else:
+ if level and (not elem.tail or not elem.tail.strip()):
+ elem.tail = i
+
+
+def start_tag(doc, name, attr=None, body=None, namespace=None):
+ """Wrapper to start an xml tag."""
+ if attr is None:
+ attr = {}
+ attr_vals = {}
+ attr_keys = {}
+ for key, val in attr.items():
+ key_tuple = (namespace, key)
+ attr_vals[key_tuple] = val
+ attr_keys[key_tuple] = key
+ attr2 = AttributesNSImpl(attr_vals, attr_keys)
+ doc.startElementNS((namespace, name), name, attr2)
+ if body:
+ doc.characters(body)
+
+
+def end_tag(doc, name, namespace=None):
+ """Wrapper to close an xml tag."""
+ doc.endElementNS((namespace, name), name)
+
+
+def tag(doc, name, attr=None, body=None, namespace=None):
+ """Wrapper to print xml tags and comments."""
+ if attr is None:
+ attr = {}
+ start_tag(doc, name, attr, body, namespace)
+ end_tag(doc, name, namespace)
diff --git a/tablib/packages/openpyxl3/style.py b/tablib/packages/openpyxl3/style.py
new file mode 100644
index 0000000..c113bd9
--- /dev/null
+++ b/tablib/packages/openpyxl3/style.py
@@ -0,0 +1,392 @@
+# file openpyxl/style.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Style and formatting option tracking."""
+
+# Python stdlib imports
+import re
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+
+
+class HashableObject(object):
+ """Define how to hash property classes."""
+ __fields__ = None
+ __leaf__ = False
+
+ def __repr__(self):
+
+ return ':'.join([repr(getattr(self, x)) for x in self.__fields__])
+
+ def __hash__(self):
+
+# return int(md5(repr(self)).hexdigest(), 16)
+ return hash(repr(self))
+
+class Color(HashableObject):
+ """Named colors for use in styles."""
+ BLACK = 'FF000000'
+ WHITE = 'FFFFFFFF'
+ RED = 'FFFF0000'
+ DARKRED = 'FF800000'
+ BLUE = 'FF0000FF'
+ DARKBLUE = 'FF000080'
+ GREEN = 'FF00FF00'
+ DARKGREEN = 'FF008000'
+ YELLOW = 'FFFFFF00'
+ DARKYELLOW = 'FF808000'
+
+ __fields__ = ('index',)
+ __slots__ = __fields__
+ __leaf__ = True
+
+ def __init__(self, index):
+ super(Color, self).__init__()
+ self.index = index
+
+
+class Font(HashableObject):
+ """Font options used in styles."""
+ UNDERLINE_NONE = 'none'
+ UNDERLINE_DOUBLE = 'double'
+ UNDERLINE_DOUBLE_ACCOUNTING = 'doubleAccounting'
+ UNDERLINE_SINGLE = 'single'
+ UNDERLINE_SINGLE_ACCOUNTING = 'singleAccounting'
+
+ __fields__ = ('name',
+ 'size',
+ 'bold',
+ 'italic',
+ 'superscript',
+ 'subscript',
+ 'underline',
+ 'strikethrough',
+ 'color')
+ __slots__ = __fields__
+
+ def __init__(self):
+ super(Font, self).__init__()
+ self.name = 'Calibri'
+ self.size = 11
+ self.bold = False
+ self.italic = False
+ self.superscript = False
+ self.subscript = False
+ self.underline = self.UNDERLINE_NONE
+ self.strikethrough = False
+ self.color = Color(Color.BLACK)
+
+
+class Fill(HashableObject):
+ """Area fill patterns for use in styles."""
+ FILL_NONE = 'none'
+ FILL_SOLID = 'solid'
+ FILL_GRADIENT_LINEAR = 'linear'
+ FILL_GRADIENT_PATH = 'path'
+ FILL_PATTERN_DARKDOWN = 'darkDown'
+ FILL_PATTERN_DARKGRAY = 'darkGray'
+ FILL_PATTERN_DARKGRID = 'darkGrid'
+ FILL_PATTERN_DARKHORIZONTAL = 'darkHorizontal'
+ FILL_PATTERN_DARKTRELLIS = 'darkTrellis'
+ FILL_PATTERN_DARKUP = 'darkUp'
+ FILL_PATTERN_DARKVERTICAL = 'darkVertical'
+ FILL_PATTERN_GRAY0625 = 'gray0625'
+ FILL_PATTERN_GRAY125 = 'gray125'
+ FILL_PATTERN_LIGHTDOWN = 'lightDown'
+ FILL_PATTERN_LIGHTGRAY = 'lightGray'
+ FILL_PATTERN_LIGHTGRID = 'lightGrid'
+ FILL_PATTERN_LIGHTHORIZONTAL = 'lightHorizontal'
+ FILL_PATTERN_LIGHTTRELLIS = 'lightTrellis'
+ FILL_PATTERN_LIGHTUP = 'lightUp'
+ FILL_PATTERN_LIGHTVERTICAL = 'lightVertical'
+ FILL_PATTERN_MEDIUMGRAY = 'mediumGray'
+
+ __fields__ = ('fill_type',
+ 'rotation',
+ 'start_color',
+ 'end_color')
+ __slots__ = __fields__
+
+ def __init__(self):
+ super(Fill, self).__init__()
+ self.fill_type = self.FILL_NONE
+ self.rotation = 0
+ self.start_color = Color(Color.WHITE)
+ self.end_color = Color(Color.BLACK)
+
+
+class Border(HashableObject):
+ """Border options for use in styles."""
+ BORDER_NONE = 'none'
+ BORDER_DASHDOT = 'dashDot'
+ BORDER_DASHDOTDOT = 'dashDotDot'
+ BORDER_DASHED = 'dashed'
+ BORDER_DOTTED = 'dotted'
+ BORDER_DOUBLE = 'double'
+ BORDER_HAIR = 'hair'
+ BORDER_MEDIUM = 'medium'
+ BORDER_MEDIUMDASHDOT = 'mediumDashDot'
+ BORDER_MEDIUMDASHDOTDOT = 'mediumDashDotDot'
+ BORDER_MEDIUMDASHED = 'mediumDashed'
+ BORDER_SLANTDASHDOT = 'slantDashDot'
+ BORDER_THICK = 'thick'
+ BORDER_THIN = 'thin'
+
+ __fields__ = ('border_style',
+ 'color')
+ __slots__ = __fields__
+
+ def __init__(self):
+ super(Border, self).__init__()
+ self.border_style = self.BORDER_NONE
+ self.color = Color(Color.BLACK)
+
+
+class Borders(HashableObject):
+ """Border positioning for use in styles."""
+ DIAGONAL_NONE = 0
+ DIAGONAL_UP = 1
+ DIAGONAL_DOWN = 2
+ DIAGONAL_BOTH = 3
+
+ __fields__ = ('left',
+ 'right',
+ 'top',
+ 'bottom',
+ 'diagonal',
+ 'diagonal_direction',
+ 'all_borders',
+ 'outline',
+ 'inside',
+ 'vertical',
+ 'horizontal')
+ __slots__ = __fields__
+
+ def __init__(self):
+ super(Borders, self).__init__()
+ self.left = Border()
+ self.right = Border()
+ self.top = Border()
+ self.bottom = Border()
+ self.diagonal = Border()
+ self.diagonal_direction = self.DIAGONAL_NONE
+
+ self.all_borders = Border()
+ self.outline = Border()
+ self.inside = Border()
+ self.vertical = Border()
+ self.horizontal = Border()
+
+
+class Alignment(HashableObject):
+ """Alignment options for use in styles."""
+ HORIZONTAL_GENERAL = 'general'
+ HORIZONTAL_LEFT = 'left'
+ HORIZONTAL_RIGHT = 'right'
+ HORIZONTAL_CENTER = 'center'
+ HORIZONTAL_CENTER_CONTINUOUS = 'centerContinuous'
+ HORIZONTAL_JUSTIFY = 'justify'
+ VERTICAL_BOTTOM = 'bottom'
+ VERTICAL_TOP = 'top'
+ VERTICAL_CENTER = 'center'
+ VERTICAL_JUSTIFY = 'justify'
+
+ __fields__ = ('horizontal',
+ 'vertical',
+ 'text_rotation',
+ 'wrap_text',
+ 'shrink_to_fit',
+ 'indent')
+ __slots__ = __fields__
+ __leaf__ = True
+
+ def __init__(self):
+ super(Alignment, self).__init__()
+ self.horizontal = self.HORIZONTAL_GENERAL
+ self.vertical = self.VERTICAL_BOTTOM
+ self.text_rotation = 0
+ self.wrap_text = False
+ self.shrink_to_fit = False
+ self.indent = 0
+
+
+class NumberFormat(HashableObject):
+ """Numer formatting for use in styles."""
+ FORMAT_GENERAL = 'General'
+ FORMAT_TEXT = '@'
+ FORMAT_NUMBER = '0'
+ FORMAT_NUMBER_00 = '0.00'
+ FORMAT_NUMBER_COMMA_SEPARATED1 = '#,##0.00'
+ FORMAT_NUMBER_COMMA_SEPARATED2 = '#,##0.00_-'
+ FORMAT_PERCENTAGE = '0%'
+ FORMAT_PERCENTAGE_00 = '0.00%'
+ FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd'
+ FORMAT_DATE_YYYYMMDD = 'yy-mm-dd'
+ FORMAT_DATE_DDMMYYYY = 'dd/mm/yy'
+ FORMAT_DATE_DMYSLASH = 'd/m/y'
+ FORMAT_DATE_DMYMINUS = 'd-m-y'
+ FORMAT_DATE_DMMINUS = 'd-m'
+ FORMAT_DATE_MYMINUS = 'm-y'
+ FORMAT_DATE_XLSX14 = 'mm-dd-yy'
+ FORMAT_DATE_XLSX15 = 'd-mmm-yy'
+ FORMAT_DATE_XLSX16 = 'd-mmm'
+ FORMAT_DATE_XLSX17 = 'mmm-yy'
+ FORMAT_DATE_XLSX22 = 'm/d/yy h:mm'
+ FORMAT_DATE_DATETIME = 'd/m/y h:mm'
+ FORMAT_DATE_TIME1 = 'h:mm AM/PM'
+ FORMAT_DATE_TIME2 = 'h:mm:ss AM/PM'
+ FORMAT_DATE_TIME3 = 'h:mm'
+ FORMAT_DATE_TIME4 = 'h:mm:ss'
+ FORMAT_DATE_TIME5 = 'mm:ss'
+ FORMAT_DATE_TIME6 = 'h:mm:ss'
+ FORMAT_DATE_TIME7 = 'i:s.S'
+ FORMAT_DATE_TIME8 = 'h:mm:ss@'
+ FORMAT_DATE_YYYYMMDDSLASH = 'yy/mm/dd@'
+ FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-'
+ FORMAT_CURRENCY_USD = '$#,##0_-'
+ FORMAT_CURRENCY_EUR_SIMPLE = '[$EUR ]#,##0.00_-'
+ _BUILTIN_FORMATS = {
+ 0: 'General',
+ 1: '0',
+ 2: '0.00',
+ 3: '#,##0',
+ 4: '#,##0.00',
+
+ 9: '0%',
+ 10: '0.00%',
+ 11: '0.00E+00',
+ 12: '# ?/?',
+ 13: '# ??/??',
+ 14: 'mm-dd-yy',
+ 15: 'd-mmm-yy',
+ 16: 'd-mmm',
+ 17: 'mmm-yy',
+ 18: 'h:mm AM/PM',
+ 19: 'h:mm:ss AM/PM',
+ 20: 'h:mm',
+ 21: 'h:mm:ss',
+ 22: 'm/d/yy h:mm',
+
+ 37: '#,##0 (#,##0)',
+ 38: '#,##0 [Red](#,##0)',
+ 39: '#,##0.00(#,##0.00)',
+ 40: '#,##0.00[Red](#,##0.00)',
+
+ 41: '_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)',
+ 42: '_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)',
+ 43: '_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)',
+
+ 44: '_("$"* #,##0.00_)_("$"* \(#,##0.00\)_("$"* "-"??_)_(@_)',
+ 45: 'mm:ss',
+ 46: '[h]:mm:ss',
+ 47: 'mmss.0',
+ 48: '##0.0E+0',
+ 49: '@', }
+ _BUILTIN_FORMATS_REVERSE = dict(
+ [(value, key) for key, value in _BUILTIN_FORMATS.items()])
+
+ __fields__ = ('_format_code',
+ '_format_index')
+ __slots__ = __fields__
+ __leaf__ = True
+
+ DATE_INDICATORS = 'dmyhs'
+
+ def __init__(self):
+ super(NumberFormat, self).__init__()
+ self._format_code = self.FORMAT_GENERAL
+ self._format_index = 0
+
+ def _set_format_code(self, format_code = FORMAT_GENERAL):
+ """Setter for the format_code property."""
+ self._format_code = format_code
+ self._format_index = self.builtin_format_id(format = format_code)
+
+ def _get_format_code(self):
+ """Getter for the format_code property."""
+ return self._format_code
+
+ format_code = property(_get_format_code, _set_format_code)
+
+ def builtin_format_code(self, index):
+ """Return one of the standard format codes by index."""
+ return self._BUILTIN_FORMATS[index]
+
+ def is_builtin(self, format = None):
+ """Check if a format code is a standard format code."""
+ if format is None:
+ format = self._format_code
+ return format in list(self._BUILTIN_FORMATS.values())
+
+ def builtin_format_id(self, format):
+ """Return the id of a standard style."""
+ return self._BUILTIN_FORMATS_REVERSE.get(format, None)
+
+ def is_date_format(self, format = None):
+ """Check if the number format is actually representing a date."""
+ if format is None:
+ format = self._format_code
+
+ return any([x in format for x in self.DATE_INDICATORS])
+
+class Protection(HashableObject):
+ """Protection options for use in styles."""
+ PROTECTION_INHERIT = 'inherit'
+ PROTECTION_PROTECTED = 'protected'
+ PROTECTION_UNPROTECTED = 'unprotected'
+
+ __fields__ = ('locked',
+ 'hidden')
+ __slots__ = __fields__
+ __leaf__ = True
+
+ def __init__(self):
+ super(Protection, self).__init__()
+ self.locked = self.PROTECTION_INHERIT
+ self.hidden = self.PROTECTION_INHERIT
+
+
+class Style(HashableObject):
+ """Style object containing all formatting details."""
+ __fields__ = ('font',
+ 'fill',
+ 'borders',
+ 'alignment',
+ 'number_format',
+ 'protection')
+ __slots__ = __fields__
+
+ def __init__(self):
+ super(Style, self).__init__()
+ self.font = Font()
+ self.fill = Fill()
+ self.borders = Borders()
+ self.alignment = Alignment()
+ self.number_format = NumberFormat()
+ self.protection = Protection()
+
+DEFAULTS = Style()
diff --git a/tablib/packages/openpyxl3/workbook.py b/tablib/packages/openpyxl3/workbook.py
new file mode 100644
index 0000000..bbb14b6
--- /dev/null
+++ b/tablib/packages/openpyxl3/workbook.py
@@ -0,0 +1,186 @@
+# file openpyxl/workbook.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Workbook is the top-level container for all document information."""
+
+__docformat__ = "restructuredtext en"
+
+# Python stdlib imports
+import datetime
+import os
+
+# package imports
+from .worksheet import Worksheet
+from .writer.dump_worksheet import DumpWorksheet, save_dump
+from .writer.strings import StringTableBuilder
+from .namedrange import NamedRange
+from .style import Style
+from .writer.excel import save_workbook
+from .shared.exc import ReadOnlyWorkbookException
+
+
+class DocumentProperties(object):
+ """High-level properties of the document."""
+
+ def __init__(self):
+ self.creator = 'Unknown'
+ self.last_modified_by = self.creator
+ self.created = datetime.datetime.now()
+ self.modified = datetime.datetime.now()
+ self.title = 'Untitled'
+ self.subject = ''
+ self.description = ''
+ self.keywords = ''
+ self.category = ''
+ self.company = 'Microsoft Corporation'
+
+
+class DocumentSecurity(object):
+ """Security information about the document."""
+
+ def __init__(self):
+ self.lock_revision = False
+ self.lock_structure = False
+ self.lock_windows = False
+ self.revision_password = ''
+ self.workbook_password = ''
+
+
+class Workbook(object):
+ """Workbook is the container for all other parts of the document."""
+
+ def __init__(self, optimized_write = False):
+ self.worksheets = []
+ self._active_sheet_index = 0
+ self._named_ranges = []
+ self.properties = DocumentProperties()
+ self.style = Style()
+ self.security = DocumentSecurity()
+ self.__optimized_write = optimized_write
+ self.__optimized_read = False
+ self.strings_table_builder = StringTableBuilder()
+
+ if not optimized_write:
+ self.worksheets.append(Worksheet(self))
+
+ def _set_optimized_read(self):
+ self.__optimized_read = True
+
+ def get_active_sheet(self):
+ """Returns the current active sheet."""
+ return self.worksheets[self._active_sheet_index]
+
+ def create_sheet(self, index = None):
+ """Create a worksheet (at an optional index).
+
+ :param index: optional position at which the sheet will be inserted
+ :type index: int
+
+ """
+
+ if self.__optimized_read:
+ raise ReadOnlyWorkbookException('Cannot create new sheet in a read-only workbook')
+
+ if self.__optimized_write :
+ new_ws = DumpWorksheet(parent_workbook = self)
+ else:
+ new_ws = Worksheet(parent_workbook = self)
+
+ self.add_sheet(worksheet = new_ws, index = index)
+ return new_ws
+
+ def add_sheet(self, worksheet, index = None):
+ """Add an existing worksheet (at an optional index)."""
+ if index is None:
+ index = len(self.worksheets)
+ self.worksheets.insert(index, worksheet)
+
+ def remove_sheet(self, worksheet):
+ """Remove a worksheet from this workbook."""
+ self.worksheets.remove(worksheet)
+
+ def get_sheet_by_name(self, name):
+ """Returns a worksheet by its name.
+
+ Returns None if no worksheet has the name specified.
+
+ :param name: the name of the worksheet to look for
+ :type name: string
+
+ """
+ requested_sheet = None
+ for sheet in self.worksheets:
+ if sheet.title == name:
+ requested_sheet = sheet
+ break
+ return requested_sheet
+
+ def get_index(self, worksheet):
+ """Return the index of the worksheet."""
+ return self.worksheets.index(worksheet)
+
+ def get_sheet_names(self):
+ """Returns the list of the names of worksheets in the workbook.
+
+ Names are returned in the worksheets order.
+
+ :rtype: list of strings
+
+ """
+ return [s.title for s in self.worksheets]
+
+ def create_named_range(self, name, worksheet, range):
+ """Create a new named_range on a worksheet"""
+ assert isinstance(worksheet, Worksheet)
+ named_range = NamedRange(name, [(worksheet, range)])
+ self.add_named_range(named_range)
+
+ def get_named_ranges(self):
+ """Return all named ranges"""
+ return self._named_ranges
+
+ def add_named_range(self, named_range):
+ """Add an existing named_range to the list of named_ranges."""
+ self._named_ranges.append(named_range)
+
+ def get_named_range(self, name):
+ """Return the range specified by name."""
+ requested_range = None
+ for named_range in self._named_ranges:
+ if named_range.name == name:
+ requested_range = named_range
+ break
+ return requested_range
+
+ def remove_named_range(self, named_range):
+ """Remove a named_range from this workbook."""
+ self._named_ranges.remove(named_range)
+
+ def save(self, filename):
+ """ shortcut """
+ if self.__optimized_write:
+ save_dump(self, filename)
+ else:
+ save_workbook(self, filename)
diff --git a/tablib/packages/openpyxl3/worksheet.py b/tablib/packages/openpyxl3/worksheet.py
new file mode 100644
index 0000000..6cdda6b
--- /dev/null
+++ b/tablib/packages/openpyxl3/worksheet.py
@@ -0,0 +1,534 @@
+# file openpyxl/worksheet.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Worksheet is the 2nd-level container in Excel."""
+
+# Python stdlib imports
+import re
+
+# package imports
+from . import cell
+from .cell import coordinate_from_string, \
+ column_index_from_string, get_column_letter
+from .shared.exc import SheetTitleException, \
+ InsufficientCoordinatesException, CellCoordinatesException, \
+ NamedRangeException
+from .shared.password_hasher import hash_password
+from .style import Style, DEFAULTS as DEFAULTS_STYLE
+from .drawing import Drawing
+
+_DEFAULTS_STYLE_HASH = hash(DEFAULTS_STYLE)
+
+def flatten(results):
+
+ rows = []
+
+ for row in results:
+
+ cells = []
+
+ for cell in row:
+
+ cells.append(cell.value)
+
+ rows.append(tuple(cells))
+
+ return tuple(rows)
+
+
+class Relationship(object):
+ """Represents many kinds of relationships."""
+ # TODO: Use this object for workbook relationships as well as
+ # worksheet relationships
+ TYPES = {
+ 'hyperlink': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
+ 'drawing':'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
+ #'worksheet': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
+ #'sharedStrings': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
+ #'styles': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
+ #'theme': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme',
+ }
+
+ def __init__(self, rel_type):
+ if rel_type not in self.TYPES:
+ raise ValueError("Invalid relationship type %s" % rel_type)
+ self.type = self.TYPES[rel_type]
+ self.target = ""
+ self.target_mode = ""
+ self.id = ""
+
+
+class PageSetup(object):
+ """Information about page layout for this sheet"""
+ pass
+
+
+class HeaderFooter(object):
+ """Information about the header/footer for this sheet."""
+ pass
+
+
+class SheetView(object):
+ """Information about the visible portions of this sheet."""
+ pass
+
+
+class RowDimension(object):
+ """Information about the display properties of a row."""
+ __slots__ = ('row_index',
+ 'height',
+ 'visible',
+ 'outline_level',
+ 'collapsed',
+ 'style_index',)
+
+ def __init__(self, index = 0):
+ self.row_index = index
+ self.height = -1
+ self.visible = True
+ self.outline_level = 0
+ self.collapsed = False
+ self.style_index = None
+
+
+class ColumnDimension(object):
+ """Information about the display properties of a column."""
+ __slots__ = ('column_index',
+ 'width',
+ 'auto_size',
+ 'visible',
+ 'outline_level',
+ 'collapsed',
+ 'style_index',)
+
+ def __init__(self, index = 'A'):
+ self.column_index = index
+ self.width = -1
+ self.auto_size = False
+ self.visible = True
+ self.outline_level = 0
+ self.collapsed = False
+ self.style_index = 0
+
+
+class PageMargins(object):
+ """Information about page margins for view/print layouts."""
+
+ def __init__(self):
+ self.left = self.right = 0.7
+ self.top = self.bottom = 0.75
+ self.header = self.footer = 0.3
+
+
+class SheetProtection(object):
+ """Information about protection of various aspects of a sheet."""
+
+ def __init__(self):
+ self.sheet = False
+ self.objects = False
+ self.scenarios = False
+ self.format_cells = False
+ self.format_columns = False
+ self.format_rows = False
+ self.insert_columns = False
+ self.insert_rows = False
+ self.insert_hyperlinks = False
+ self.delete_columns = False
+ self.delete_rows = False
+ self.select_locked_cells = False
+ self.sort = False
+ self.auto_filter = False
+ self.pivot_tables = False
+ self.select_unlocked_cells = False
+ self._password = ''
+
+ def set_password(self, value = '', already_hashed = False):
+ """Set a password on this sheet."""
+ if not already_hashed:
+ value = hash_password(value)
+ self._password = value
+
+ def _set_raw_password(self, value):
+ """Set a password directly, forcing a hash step."""
+ self.set_password(value, already_hashed = False)
+
+ def _get_raw_password(self):
+ """Return the password value, regardless of hash."""
+ return self._password
+
+ password = property(_get_raw_password, _set_raw_password,
+ 'get/set the password (if already hashed, '
+ 'use set_password() instead)')
+
+
+class Worksheet(object):
+ """Represents a worksheet.
+
+ Do not create worksheets yourself,
+ use :func:`.workbook.Workbook.create_sheet` instead
+
+ """
+ BREAK_NONE = 0
+ BREAK_ROW = 1
+ BREAK_COLUMN = 2
+
+ SHEETSTATE_VISIBLE = 'visible'
+ SHEETSTATE_HIDDEN = 'hidden'
+ SHEETSTATE_VERYHIDDEN = 'veryHidden'
+
+ def __init__(self, parent_workbook, title = 'Sheet'):
+ self._parent = parent_workbook
+ self._title = ''
+ if not title:
+ self.title = 'Sheet%d' % (1 + len(self._parent.worksheets))
+ else:
+ self.title = title
+ self.row_dimensions = {}
+ self.column_dimensions = {}
+ self._cells = {}
+ self._styles = {}
+ self._charts = []
+ self.relationships = []
+ self.selected_cell = 'A1'
+ self.active_cell = 'A1'
+ self.sheet_state = self.SHEETSTATE_VISIBLE
+ self.page_setup = PageSetup()
+ self.page_margins = PageMargins()
+ self.header_footer = HeaderFooter()
+ self.sheet_view = SheetView()
+ self.protection = SheetProtection()
+ self.show_gridlines = True
+ self.print_gridlines = False
+ self.show_summary_below = True
+ self.show_summary_right = True
+ self.default_row_dimension = RowDimension()
+ self.default_column_dimension = ColumnDimension()
+ self._auto_filter = None
+ self._freeze_panes = None
+
+ def __repr__(self):
+ return '<Worksheet "%s">' % self.title
+
+ def garbage_collect(self):
+ """Delete cells that are not storing a value."""
+ delete_list = [coordinate for coordinate, cell in \
+ self._cells.items() if (cell.value in ('', None) and \
+ hash(cell.style) == _DEFAULTS_STYLE_HASH)]
+ for coordinate in delete_list:
+ del self._cells[coordinate]
+
+ def get_cell_collection(self):
+ """Return an unordered list of the cells in this worksheet."""
+ return list(self._cells.values())
+
+ def _set_title(self, value):
+ """Set a sheet title, ensuring it is valid."""
+ bad_title_char_re = re.compile(r'[\\*?:/\[\]]')
+ if bad_title_char_re.search(value):
+ msg = 'Invalid character found in sheet title'
+ raise SheetTitleException(msg)
+
+ # check if sheet_name already exists
+ # do this *before* length check
+ if self._parent.get_sheet_by_name(value):
+ # use name, but append with lowest possible integer
+ i = 1
+ while self._parent.get_sheet_by_name('%s%d' % (value, i)):
+ i += 1
+ value = '%s%d' % (value, i)
+ if len(value) > 31:
+ msg = 'Maximum 31 characters allowed in sheet title'
+ raise SheetTitleException(msg)
+ self._title = value
+
+ def _get_title(self):
+ """Return the title for this sheet."""
+ return self._title
+
+ title = property(_get_title, _set_title, doc =
+ 'Get or set the title of the worksheet. '
+ 'Limited to 31 characters, no special characters.')
+
+ def _set_auto_filter(self, range):
+ # Normalize range to a str or None
+ if not range:
+ range = None
+ elif isinstance(range, str):
+ range = range.upper()
+ else: # Assume a range
+ range = range[0][0].address + ':' + range[-1][-1].address
+ self._auto_filter = range
+
+ def _get_auto_filter(self):
+ return self._auto_filter
+
+ auto_filter = property(_get_auto_filter, _set_auto_filter, doc =
+ 'get or set auto filtering on columns')
+ def _set_freeze_panes(self, topLeftCell):
+ if not topLeftCell:
+ topLeftCell = None
+ elif isinstance(topLeftCell, str):
+ topLeftCell = topLeftCell.upper()
+ else: # Assume a cell
+ topLeftCell = topLeftCell.address
+ if topLeftCell == 'A1':
+ topLeftCell = None
+ self._freeze_panes = topLeftCell
+
+ def _get_freeze_panes(self):
+ return self._freeze_panes
+
+ freeze_panes = property(_get_freeze_panes,_set_freeze_panes, doc =
+ "Get or set frozen panes")
+
+ def cell(self, coordinate = None, row = None, column = None):
+ """Returns a cell object based on the given coordinates.
+
+ Usage: cell(coodinate='A15') **or** cell(row=15, column=1)
+
+ If `coordinates` are not given, then row *and* column must be given.
+
+ Cells are kept in a dictionary which is empty at the worksheet
+ creation. Calling `cell` creates the cell in memory when they
+ are first accessed, to reduce memory usage.
+
+ :param coordinate: coordinates of the cell (e.g. 'B12')
+ :type coordinate: string
+
+ :param row: row index of the cell (e.g. 4)
+ :type row: int
+
+ :param column: column index of the cell (e.g. 3)
+ :type column: int
+
+ :raise: InsufficientCoordinatesException when coordinate or (row and column) are not given
+
+ :rtype: :class:`.cell.Cell`
+
+ """
+ if not coordinate:
+ if (row is None or column is None):
+ msg = "You have to provide a value either for " \
+ "'coordinate' or for 'row' *and* 'column'"
+ raise InsufficientCoordinatesException(msg)
+ else:
+ coordinate = '%s%s' % (get_column_letter(column + 1), row + 1)
+ else:
+ coordinate = coordinate.replace('$', '')
+
+ return self._get_cell(coordinate)
+
+ def _get_cell(self, coordinate):
+
+ if not coordinate in self._cells:
+ column, row = coordinate_from_string(coordinate)
+ new_cell = cell.Cell(self, column, row)
+ self._cells[coordinate] = new_cell
+ if column not in self.column_dimensions:
+ self.column_dimensions[column] = ColumnDimension(column)
+ if row not in self.row_dimensions:
+ self.row_dimensions[row] = RowDimension(row)
+ return self._cells[coordinate]
+
+ def get_highest_row(self):
+ """Returns the maximum row index containing data
+
+ :rtype: int
+ """
+ if self.row_dimensions:
+ return max(self.row_dimensions.keys())
+ else:
+ return 1
+
+ def get_highest_column(self):
+ """Get the largest value for column currently stored.
+
+ :rtype: int
+ """
+ if self.column_dimensions:
+ return max([column_index_from_string(column_index)
+ for column_index in self.column_dimensions])
+ else:
+ return 1
+
+ def calculate_dimension(self):
+ """Return the minimum bounding range for all cells containing data."""
+ return 'A1:%s%d' % (get_column_letter(self.get_highest_column()),
+ self.get_highest_row())
+
+ def range(self, range_string, row = 0, column = 0):
+ """Returns a 2D array of cells, with optional row and column offsets.
+
+ :param range_string: cell range string or `named range` name
+ :type range_string: string
+
+ :param row: number of rows to offset
+ :type row: int
+
+ :param column: number of columns to offset
+ :type column: int
+
+ :rtype: tuples of tuples of :class:`.cell.Cell`
+
+ """
+ if ':' in range_string:
+ # R1C1 range
+ result = []
+ min_range, max_range = range_string.split(':')
+ min_col, min_row = coordinate_from_string(min_range)
+ max_col, max_row = coordinate_from_string(max_range)
+ if column:
+ min_col = get_column_letter(
+ column_index_from_string(min_col) + column)
+ max_col = get_column_letter(
+ column_index_from_string(max_col) + column)
+ min_col = column_index_from_string(min_col)
+ max_col = column_index_from_string(max_col)
+ cache_cols = {}
+ for col in range(min_col, max_col + 1):
+ cache_cols[col] = get_column_letter(col)
+ rows = range(min_row + row, max_row + row + 1)
+ cols = range(min_col, max_col + 1)
+ for row in rows:
+ new_row = []
+ for col in cols:
+ new_row.append(self.cell('%s%s' % (cache_cols[col], row)))
+ result.append(tuple(new_row))
+ return tuple(result)
+ else:
+ try:
+ return self.cell(coordinate = range_string, row = row,
+ column = column)
+ except CellCoordinatesException:
+ pass
+
+ # named range
+ named_range = self._parent.get_named_range(range_string)
+ if named_range is None:
+ msg = '%s is not a valid range name' % range_string
+ raise NamedRangeException(msg)
+
+ result = []
+ for destination in named_range.destinations:
+
+ worksheet, cells_range = destination
+
+ if worksheet is not self:
+ msg = 'Range %s is not defined on worksheet %s' % \
+ (cells_range, self.title)
+ raise NamedRangeException(msg)
+
+ content = self.range(cells_range)
+
+ if isinstance(content, tuple):
+ for cells in content:
+ result.extend(cells)
+ else:
+ result.append(content)
+
+ if len(result) == 1:
+ return result[0]
+ else:
+ return tuple(result)
+
+ def get_style(self, coordinate):
+ """Return the style object for the specified cell."""
+ if not coordinate in self._styles:
+ self._styles[coordinate] = Style()
+ return self._styles[coordinate]
+
+ def create_relationship(self, rel_type):
+ """Add a relationship for this sheet."""
+ rel = Relationship(rel_type)
+ self.relationships.append(rel)
+ rel_id = self.relationships.index(rel)
+ rel.id = 'rId' + str(rel_id + 1)
+ return self.relationships[rel_id]
+
+ def add_chart(self, chart):
+ """ Add a chart to the sheet """
+
+ chart._sheet = self
+ self._charts.append(chart)
+
+ def append(self, list_or_dict):
+ """Appends a group of values at the bottom of the current sheet.
+
+ * If it's a list: all values are added in order, starting from the first column
+ * If it's a dict: values are assigned to the columns indicated by the keys (numbers or letters)
+
+ :param list_or_dict: list or dict containing values to append
+ :type list_or_dict: list/tuple or dict
+
+ Usage:
+
+ * append(['This is A1', 'This is B1', 'This is C1'])
+ * **or** append({'A' : 'This is A1', 'C' : 'This is C1'})
+ * **or** append({0 : 'This is A1', 2 : 'This is C1'})
+
+ :raise: TypeError when list_or_dict is neither a list/tuple nor a dict
+
+ """
+
+ row_idx = len(self.row_dimensions)
+
+ if isinstance(list_or_dict, (list, tuple)):
+
+ for col_idx, content in enumerate(list_or_dict):
+
+ self.cell(row = row_idx, column = col_idx).value = content
+
+ elif isinstance(list_or_dict, dict):
+
+ for col_idx, content in list_or_dict.items():
+
+ if isinstance(col_idx, str):
+ col_idx = column_index_from_string(col_idx) - 1
+
+ self.cell(row = row_idx, column = col_idx).value = content
+
+ else:
+ raise TypeError('list_or_dict must be a list or a dict')
+
+ @property
+ def rows(self):
+
+ return self.range(self.calculate_dimension())
+
+ @property
+ def columns(self):
+
+ max_row = self.get_highest_row()
+
+ cols = []
+
+ for col_idx in range(self.get_highest_column()):
+ col = get_column_letter(col_idx+1)
+ res = self.range('%s1:%s%d' % (col, col, max_row))
+ cols.append(tuple([x[0] for x in res]))
+
+
+ return tuple(cols)
+
diff --git a/tablib/packages/openpyxl3/writer/__init__.py b/tablib/packages/openpyxl3/writer/__init__.py
new file mode 100644
index 0000000..9eb0a21
--- /dev/null
+++ b/tablib/packages/openpyxl3/writer/__init__.py
@@ -0,0 +1,34 @@
+# file openpyxl/writer/__init__.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Imports for the openpyxl.writer namespace."""
+
+# package imports
+from . import excel
+from . import strings
+from . import styles
+from . import theme
+from . import workbook
+from . import worksheet
diff --git a/tablib/packages/openpyxl3/writer/charts.py b/tablib/packages/openpyxl3/writer/charts.py
new file mode 100644
index 0000000..420328d
--- /dev/null
+++ b/tablib/packages/openpyxl3/writer/charts.py
@@ -0,0 +1,262 @@
+# coding=UTF-8
+'''
+Copyright (c) 2010 openpyxl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+@license: http://www.opensource.org/licenses/mit-license.php
+@author: Eric Gazoni
+'''
+
+from ..shared.xmltools import Element, SubElement, get_document_content
+from ..chart import Chart, ErrorBar
+
+
+class ChartWriter(object):
+
+ def __init__(self, chart):
+ self.chart = chart
+
+ def write(self):
+ """ write a chart """
+
+ root = Element('c:chartSpace',
+ {'xmlns:c':"http://schemas.openxmlformats.org/drawingml/2006/chart",
+ 'xmlns:a':"http://schemas.openxmlformats.org/drawingml/2006/main",
+ 'xmlns:r':"http://schemas.openxmlformats.org/officeDocument/2006/relationships"})
+
+ SubElement(root, 'c:lang', {'val':self.chart.lang})
+ self._write_chart(root)
+ self._write_print_settings(root)
+ self._write_shapes(root)
+
+ return get_document_content(root)
+
+ def _write_chart(self, root):
+
+ chart = self.chart
+
+ ch = SubElement(root, 'c:chart')
+ self._write_title(ch)
+ plot_area = SubElement(ch, 'c:plotArea')
+ layout = SubElement(plot_area, 'c:layout')
+ mlayout = SubElement(layout, 'c:manualLayout')
+ SubElement(mlayout, 'c:layoutTarget', {'val':'inner'})
+ SubElement(mlayout, 'c:xMode', {'val':'edge'})
+ SubElement(mlayout, 'c:yMode', {'val':'edge'})
+ SubElement(mlayout, 'c:x', {'val':str(chart._get_margin_left())})
+ SubElement(mlayout, 'c:y', {'val':str(chart._get_margin_top())})
+ SubElement(mlayout, 'c:w', {'val':str(chart.width)})
+ SubElement(mlayout, 'c:h', {'val':str(chart.height)})
+
+ if chart.type == Chart.SCATTER_CHART:
+ subchart = SubElement(plot_area, 'c:scatterChart')
+ SubElement(subchart, 'c:scatterStyle', {'val':str('lineMarker')})
+ else:
+ if chart.type == Chart.BAR_CHART:
+ subchart = SubElement(plot_area, 'c:barChart')
+ SubElement(subchart, 'c:barDir', {'val':'col'})
+ else:
+ subchart = SubElement(plot_area, 'c:lineChart')
+
+ SubElement(subchart, 'c:grouping', {'val':chart.grouping})
+
+ self._write_series(subchart)
+
+ SubElement(subchart, 'c:marker', {'val':'1'})
+ SubElement(subchart, 'c:axId', {'val':str(chart.x_axis.id)})
+ SubElement(subchart, 'c:axId', {'val':str(chart.y_axis.id)})
+
+ if chart.type == Chart.SCATTER_CHART:
+ self._write_axis(plot_area, chart.x_axis, 'c:valAx')
+ else:
+ self._write_axis(plot_area, chart.x_axis, 'c:catAx')
+ self._write_axis(plot_area, chart.y_axis, 'c:valAx')
+
+ self._write_legend(ch)
+
+ SubElement(ch, 'c:plotVisOnly', {'val':'1'})
+
+ def _write_title(self, chart):
+ if self.chart.title != '':
+ title = SubElement(chart, 'c:title')
+ tx = SubElement(title, 'c:tx')
+ rich = SubElement(tx, 'c:rich')
+ SubElement(rich, 'a:bodyPr')
+ SubElement(rich, 'a:lstStyle')
+ p = SubElement(rich, 'a:p')
+ pPr = SubElement(p, 'a:pPr')
+ SubElement(pPr, 'a:defRPr')
+ r = SubElement(p, 'a:r')
+ SubElement(r, 'a:rPr', {'lang':self.chart.lang})
+ t = SubElement(r, 'a:t').text = self.chart.title
+ SubElement(title, 'c:layout')
+
+ def _write_axis(self, plot_area, axis, label):
+
+ ax = SubElement(plot_area, label)
+ SubElement(ax, 'c:axId', {'val':str(axis.id)})
+
+ scaling = SubElement(ax, 'c:scaling')
+ SubElement(scaling, 'c:orientation', {'val':axis.orientation})
+ if label == 'c:valAx':
+ SubElement(scaling, 'c:max', {'val':str(axis.max)})
+ SubElement(scaling, 'c:min', {'val':str(axis.min)})
+
+ SubElement(ax, 'c:axPos', {'val':axis.position})
+ if label == 'c:valAx':
+ SubElement(ax, 'c:majorGridlines')
+ SubElement(ax, 'c:numFmt', {'formatCode':"General", 'sourceLinked':'1'})
+ SubElement(ax, 'c:tickLblPos', {'val':axis.tick_label_position})
+ SubElement(ax, 'c:crossAx', {'val':str(axis.cross)})
+ SubElement(ax, 'c:crosses', {'val':axis.crosses})
+ if axis.auto:
+ SubElement(ax, 'c:auto', {'val':'1'})
+ if axis.label_align:
+ SubElement(ax, 'c:lblAlgn', {'val':axis.label_align})
+ if axis.label_offset:
+ SubElement(ax, 'c:lblOffset', {'val':str(axis.label_offset)})
+ if label == 'c:valAx':
+ if self.chart.type == Chart.SCATTER_CHART:
+ SubElement(ax, 'c:crossBetween', {'val':'midCat'})
+ else:
+ SubElement(ax, 'c:crossBetween', {'val':'between'})
+ SubElement(ax, 'c:majorUnit', {'val':str(axis.unit)})
+
+ def _write_series(self, subchart):
+
+ for i, serie in enumerate(self.chart._series):
+ ser = SubElement(subchart, 'c:ser')
+ SubElement(ser, 'c:idx', {'val':str(i)})
+ SubElement(ser, 'c:order', {'val':str(i)})
+
+ if serie.legend:
+ tx = SubElement(ser, 'c:tx')
+ self._write_serial(tx, serie.legend)
+
+ if serie.color:
+ sppr = SubElement(ser, 'c:spPr')
+ if self.chart.type == Chart.BAR_CHART:
+ # fill color
+ fillc = SubElement(sppr, 'a:solidFill')
+ SubElement(fillc, 'a:srgbClr', {'val':serie.color})
+ # edge color
+ ln = SubElement(sppr, 'a:ln')
+ fill = SubElement(ln, 'a:solidFill')
+ SubElement(fill, 'a:srgbClr', {'val':serie.color})
+
+ if serie.error_bar:
+ self._write_error_bar(ser, serie)
+
+ marker = SubElement(ser, 'c:marker')
+ SubElement(marker, 'c:symbol', {'val':serie.marker})
+
+ if serie.labels:
+ cat = SubElement(ser, 'c:cat')
+ self._write_serial(cat, serie.labels)
+
+ if self.chart.type == Chart.SCATTER_CHART:
+ if serie.xvalues:
+ xval = SubElement(ser, 'c:xVal')
+ self._write_serial(xval, serie.xvalues)
+
+ yval = SubElement(ser, 'c:yVal')
+ self._write_serial(yval, serie.values)
+ else:
+ val = SubElement(ser, 'c:val')
+ self._write_serial(val, serie.values)
+
+ def _write_serial(self, node, serie, literal=False):
+
+ cache = serie._get_cache()
+ if isinstance(cache[0], str):
+ typ = 'str'
+ else:
+ typ = 'num'
+
+ if not literal:
+ if typ == 'num':
+ ref = SubElement(node, 'c:numRef')
+ else:
+ ref = SubElement(node, 'c:strRef')
+ SubElement(ref, 'c:f').text = serie._get_ref()
+ if typ == 'num':
+ data = SubElement(ref, 'c:numCache')
+ else:
+ data = SubElement(ref, 'c:strCache')
+ else:
+ data = SubElement(node, 'c:numLit')
+
+ if typ == 'num':
+ SubElement(data, 'c:formatCode').text = 'General'
+ if literal:
+ values = (1,)
+ else:
+ values = cache
+
+ SubElement(data, 'c:ptCount', {'val':str(len(values))})
+ for j, val in enumerate(values):
+ point = SubElement(data, 'c:pt', {'idx':str(j)})
+ SubElement(point, 'c:v').text = str(val)
+
+ def _write_error_bar(self, node, serie):
+
+ flag = {ErrorBar.PLUS_MINUS:'both',
+ ErrorBar.PLUS:'plus',
+ ErrorBar.MINUS:'minus'}
+
+ eb = SubElement(node, 'c:errBars')
+ SubElement(eb, 'c:errBarType', {'val':flag[serie.error_bar.type]})
+ SubElement(eb, 'c:errValType', {'val':'cust'})
+
+ plus = SubElement(eb, 'c:plus')
+ self._write_serial(plus, serie.error_bar.values,
+ literal=(serie.error_bar.type==ErrorBar.MINUS))
+
+ minus = SubElement(eb, 'c:minus')
+ self._write_serial(minus, serie.error_bar.values,
+ literal=(serie.error_bar.type==ErrorBar.PLUS))
+
+ def _write_legend(self, chart):
+
+ legend = SubElement(chart, 'c:legend')
+ SubElement(legend, 'c:legendPos', {'val':self.chart.legend.position})
+ SubElement(legend, 'c:layout')
+
+ def _write_print_settings(self, root):
+
+ settings = SubElement(root, 'c:printSettings')
+ SubElement(settings, 'c:headerFooter')
+ margins = dict([(k, str(v)) for (k,v) in self.chart.print_margins.items()])
+ SubElement(settings, 'c:pageMargins', margins)
+ SubElement(settings, 'c:pageSetup')
+
+ def _write_shapes(self, root):
+
+ if self.chart._shapes:
+ SubElement(root, 'c:userShapes', {'r:id':'rId1'})
+
+ def write_rels(self, drawing_id):
+
+ root = Element('Relationships', {'xmlns' : 'http://schemas.openxmlformats.org/package/2006/relationships'})
+ attrs = {'Id' : 'rId1',
+ 'Type' : 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUserShapes',
+ 'Target' : '../drawings/drawing%s.xml' % drawing_id }
+ SubElement(root, 'Relationship', attrs)
+ return get_document_content(root)
diff --git a/tablib/packages/openpyxl3/writer/drawings.py b/tablib/packages/openpyxl3/writer/drawings.py
new file mode 100644
index 0000000..8a6cce2
--- /dev/null
+++ b/tablib/packages/openpyxl3/writer/drawings.py
@@ -0,0 +1,192 @@
+# coding=UTF-8
+'''
+Copyright (c) 2010 openpyxl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+@license: http://www.opensource.org/licenses/mit-license.php
+@author: Eric Gazoni
+'''
+
+from ..shared.xmltools import Element, SubElement, get_document_content
+
+
+class DrawingWriter(object):
+ """ one main drawing file per sheet """
+
+ def __init__(self, sheet):
+ self._sheet = sheet
+
+ def write(self):
+ """ write drawings for one sheet in one file """
+
+ root = Element('xdr:wsDr',
+ {'xmlns:xdr' : "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing",
+ 'xmlns:a' : "http://schemas.openxmlformats.org/drawingml/2006/main"})
+
+ for i, chart in enumerate(self._sheet._charts):
+
+ drawing = chart.drawing
+
+# anchor = SubElement(root, 'xdr:twoCellAnchor')
+# (start_row, start_col), (end_row, end_col) = drawing.coordinates
+# # anchor coordinates
+# _from = SubElement(anchor, 'xdr:from')
+# x = SubElement(_from, 'xdr:col').text = str(start_col)
+# x = SubElement(_from, 'xdr:colOff').text = '0'
+# x = SubElement(_from, 'xdr:row').text = str(start_row)
+# x = SubElement(_from, 'xdr:rowOff').text = '0'
+
+# _to = SubElement(anchor, 'xdr:to')
+# x = SubElement(_to, 'xdr:col').text = str(end_col)
+# x = SubElement(_to, 'xdr:colOff').text = '0'
+# x = SubElement(_to, 'xdr:row').text = str(end_row)
+# x = SubElement(_to, 'xdr:rowOff').text = '0'
+
+ # we only support absolute anchor atm (TODO: oneCellAnchor, twoCellAnchor
+ x, y, w, h = drawing.get_emu_dimensions()
+ anchor = SubElement(root, 'xdr:absoluteAnchor')
+ SubElement(anchor, 'xdr:pos', {'x':str(x), 'y':str(y)})
+ SubElement(anchor, 'xdr:ext', {'cx':str(w), 'cy':str(h)})
+
+ # graph frame
+ frame = SubElement(anchor, 'xdr:graphicFrame', {'macro':''})
+
+ name = SubElement(frame, 'xdr:nvGraphicFramePr')
+ SubElement(name, 'xdr:cNvPr', {'id':'%s' % i, 'name':'Graphique %s' % i})
+ SubElement(name, 'xdr:cNvGraphicFramePr')
+
+ frm = SubElement(frame, 'xdr:xfrm')
+ # no transformation
+ SubElement(frm, 'a:off', {'x':'0', 'y':'0'})
+ SubElement(frm, 'a:ext', {'cx':'0', 'cy':'0'})
+
+ graph = SubElement(frame, 'a:graphic')
+ data = SubElement(graph, 'a:graphicData',
+ {'uri':'http://schemas.openxmlformats.org/drawingml/2006/chart'})
+ SubElement(data, 'c:chart',
+ { 'xmlns:c':'http://schemas.openxmlformats.org/drawingml/2006/chart',
+ 'xmlns:r':'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
+ 'r:id':'rId%s' % (i + 1)})
+
+ SubElement(anchor, 'xdr:clientData')
+
+ return get_document_content(root)
+
+ def write_rels(self, chart_id):
+
+ root = Element('Relationships',
+ {'xmlns' : 'http://schemas.openxmlformats.org/package/2006/relationships'})
+ for i, chart in enumerate(self._sheet._charts):
+ attrs = {'Id' : 'rId%s' % (i + 1),
+ 'Type' : 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart',
+ 'Target' : '../charts/chart%s.xml' % (chart_id + i) }
+ SubElement(root, 'Relationship', attrs)
+ return get_document_content(root)
+
+class ShapeWriter(object):
+ """ one file per shape """
+
+ schema = "http://schemas.openxmlformats.org/drawingml/2006/main"
+
+ def __init__(self, shapes):
+
+ self._shapes = shapes
+
+ def write(self, shape_id):
+
+ root = Element('c:userShapes', {'xmlns:c' : 'http://schemas.openxmlformats.org/drawingml/2006/chart'})
+
+ for shape in self._shapes:
+ anchor = SubElement(root, 'cdr:relSizeAnchor',
+ {'xmlns:cdr' : "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"})
+
+ xstart, ystart, xend, yend = shape.get_coordinates()
+
+ _from = SubElement(anchor, 'cdr:from')
+ SubElement(_from, 'cdr:x').text = str(xstart)
+ SubElement(_from, 'cdr:y').text = str(ystart)
+
+ _to = SubElement(anchor, 'cdr:to')
+ SubElement(_to, 'cdr:x').text = str(xend)
+ SubElement(_to, 'cdr:y').text = str(yend)
+
+ sp = SubElement(anchor, 'cdr:sp', {'macro':'', 'textlink':''})
+ nvspr = SubElement(sp, 'cdr:nvSpPr')
+ SubElement(nvspr, 'cdr:cNvPr', {'id':str(shape_id), 'name':'shape %s' % shape_id})
+ SubElement(nvspr, 'cdr:cNvSpPr')
+
+ sppr = SubElement(sp, 'cdr:spPr')
+ frm = SubElement(sppr, 'a:xfrm', {'xmlns:a':self.schema})
+ # no transformation
+ SubElement(frm, 'a:off', {'x':'0', 'y':'0'})
+ SubElement(frm, 'a:ext', {'cx':'0', 'cy':'0'})
+
+ prstgeom = SubElement(sppr, 'a:prstGeom', {'xmlns:a':self.schema, 'prst':str(shape.style)})
+ SubElement(prstgeom, 'a:avLst')
+
+ fill = SubElement(sppr, 'a:solidFill', {'xmlns:a':self.schema})
+ SubElement(fill, 'a:srgbClr', {'val':shape.color})
+
+ border = SubElement(sppr, 'a:ln', {'xmlns:a':self.schema, 'w':str(shape._border_width)})
+ sf = SubElement(border, 'a:solidFill')
+ SubElement(sf, 'a:srgbClr', {'val':shape.border_color})
+
+ self._write_style(sp)
+ self._write_text(sp, shape)
+
+ shape_id += 1
+
+ return get_document_content(root)
+
+ def _write_text(self, node, shape):
+ """ write text in the shape """
+
+ tx_body = SubElement(node, 'cdr:txBody')
+ SubElement(tx_body, 'a:bodyPr', {'xmlns:a':self.schema, 'vertOverflow':'clip'})
+ SubElement(tx_body, 'a:lstStyle',
+ {'xmlns:a':self.schema})
+ p = SubElement(tx_body, 'a:p', {'xmlns:a':self.schema})
+ if shape.text:
+ r = SubElement(p, 'a:r')
+ rpr = SubElement(r, 'a:rPr', {'lang':'en-US'})
+ fill = SubElement(rpr, 'a:solidFill')
+ SubElement(fill, 'a:srgbClr', {'val':shape.text_color})
+
+ SubElement(r, 'a:t').text = shape.text
+ else:
+ SubElement(p, 'a:endParaRPr', {'lang':'en-US'})
+
+ def _write_style(self, node):
+ """ write style theme """
+
+ style = SubElement(node, 'cdr:style')
+
+ ln_ref = SubElement(style, 'a:lnRef', {'xmlns:a':self.schema, 'idx':'2'})
+ scheme_clr = SubElement(ln_ref, 'a:schemeClr', {'val':'accent1'})
+ SubElement(scheme_clr, 'a:shade', {'val':'50000'})
+
+ fill_ref = SubElement(style, 'a:fillRef', {'xmlns:a':self.schema, 'idx':'1'})
+ SubElement(fill_ref, 'a:schemeClr', {'val':'accent1'})
+
+ effect_ref = SubElement(style, 'a:effectRef', {'xmlns:a':self.schema, 'idx':'0'})
+ SubElement(effect_ref, 'a:schemeClr', {'val':'accent1'})
+
+ font_ref = SubElement(style, 'a:fontRef', {'xmlns:a':self.schema, 'idx':'minor'})
+ SubElement(font_ref, 'a:schemeClr', {'val':'lt1'})
diff --git a/tablib/packages/openpyxl3/writer/dump_worksheet.py b/tablib/packages/openpyxl3/writer/dump_worksheet.py
new file mode 100644
index 0000000..36d68d4
--- /dev/null
+++ b/tablib/packages/openpyxl3/writer/dump_worksheet.py
@@ -0,0 +1,256 @@
+# file openpyxl/writer/straight_worksheet.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write worksheets to xml representations in an optimized way"""
+
+import datetime
+import os
+
+from ..cell import column_index_from_string, get_column_letter, Cell
+from ..worksheet import Worksheet
+from ..shared.xmltools import XMLGenerator, get_document_content, \
+ start_tag, end_tag, tag
+from ..shared.date_time import SharedDate
+from ..shared.ooxml import MAX_COLUMN, MAX_ROW
+from tempfile import NamedTemporaryFile
+from ..writer.excel import ExcelWriter
+from ..writer.strings import write_string_table
+from ..writer.styles import StyleWriter
+from ..style import Style, NumberFormat
+
+from ..shared.ooxml import ARC_SHARED_STRINGS, ARC_CONTENT_TYPES, \
+ ARC_ROOT_RELS, ARC_WORKBOOK_RELS, ARC_APP, ARC_CORE, ARC_THEME, \
+ ARC_STYLE, ARC_WORKBOOK, \
+ PACKAGE_WORKSHEETS, PACKAGE_DRAWINGS, PACKAGE_CHARTS
+
+STYLES = {'datetime' : {'type':Cell.TYPE_NUMERIC,
+ 'style':'1'},
+ 'string':{'type':Cell.TYPE_STRING,
+ 'style':'0'},
+ 'numeric':{'type':Cell.TYPE_NUMERIC,
+ 'style':'0'},
+ 'formula':{'type':Cell.TYPE_FORMULA,
+ 'style':'0'},
+ 'boolean':{'type':Cell.TYPE_BOOL,
+ 'style':'0'},
+ }
+
+DATETIME_STYLE = Style()
+DATETIME_STYLE.number_format.format_code = NumberFormat.FORMAT_DATE_YYYYMMDD2
+BOUNDING_BOX_PLACEHOLDER = 'A1:%s%d' % (get_column_letter(MAX_COLUMN), MAX_ROW)
+
+class DumpWorksheet(Worksheet):
+
+ """
+ .. warning::
+
+ You shouldn't initialize this yourself, use :class:`..workbook.Workbook` constructor instead,
+ with `optimized_write = True`.
+ """
+
+ def __init__(self, parent_workbook):
+
+ Worksheet.__init__(self, parent_workbook)
+
+ self._max_col = 0
+ self._max_row = 0
+ self._parent = parent_workbook
+ self._fileobj_header = NamedTemporaryFile(mode='r+', prefix='..', suffix='.header', delete=False)
+ self._fileobj_content = NamedTemporaryFile(mode='r+', prefix='..', suffix='.content', delete=False)
+ self._fileobj = NamedTemporaryFile(mode='w', prefix='..', delete=False)
+ self.doc = XMLGenerator(self._fileobj_content, 'utf-8')
+ self.header = XMLGenerator(self._fileobj_header, 'utf-8')
+ self.title = 'Sheet'
+
+ self._shared_date = SharedDate()
+ self._string_builder = self._parent.strings_table_builder
+
+ @property
+ def filename(self):
+ return self._fileobj.name
+
+ def write_header(self):
+
+ doc = self.header
+
+ start_tag(doc, 'worksheet',
+ {'xml:space': 'preserve',
+ 'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'})
+ start_tag(doc, 'sheetPr')
+ tag(doc, 'outlinePr',
+ {'summaryBelow': '1',
+ 'summaryRight': '1'})
+ end_tag(doc, 'sheetPr')
+ tag(doc, 'dimension', {'ref': 'A1:%s' % (self.get_dimensions())})
+ start_tag(doc, 'sheetViews')
+ start_tag(doc, 'sheetView', {'workbookViewId': '0'})
+ tag(doc, 'selection', {'activeCell': 'A1',
+ 'sqref': 'A1'})
+ end_tag(doc, 'sheetView')
+ end_tag(doc, 'sheetViews')
+ tag(doc, 'sheetFormatPr', {'defaultRowHeight': '15'})
+ start_tag(doc, 'sheetData')
+
+ def close(self):
+
+ self._close_content()
+ self._close_header()
+
+ self._write_fileobj(self._fileobj_header)
+ self._write_fileobj(self._fileobj_content)
+
+ self._fileobj.close()
+
+ def _write_fileobj(self, fobj):
+
+ fobj.flush()
+ fobj.seek(0)
+
+ while True:
+ chunk = fobj.read(4096)
+ if not chunk:
+ break
+ self._fileobj.write(chunk)
+
+ fobj.close()
+ os.remove(fobj.name)
+
+ self._fileobj.flush()
+
+ def _close_header(self):
+
+ doc = self.header
+ #doc.endDocument()
+
+ def _close_content(self):
+
+ doc = self.doc
+ end_tag(doc, 'sheetData')
+
+ end_tag(doc, 'worksheet')
+ #doc.endDocument()
+
+ def get_dimensions(self):
+
+ if not self._max_col or not self._max_row:
+ return 'A1'
+ else:
+ return '%s%d' % (get_column_letter(self._max_col), (self._max_row))
+
+ def append(self, row):
+
+ """
+ :param row: iterable containing values to append
+ :type row: iterable
+ """
+
+ doc = self.doc
+
+ self._max_row += 1
+ span = len(row)
+ self._max_col = max(self._max_col, span)
+
+ row_idx = self._max_row
+
+ attrs = {'r': '%d' % row_idx,
+ 'spans': '1:%d' % span}
+
+ start_tag(doc, 'row', attrs)
+
+ for col_idx, cell in enumerate(row):
+
+ if cell is None:
+ continue
+
+ coordinate = '%s%d' % (get_column_letter(col_idx+1), row_idx)
+ attributes = {'r': coordinate}
+
+ if isinstance(cell, bool):
+ dtype = 'boolean'
+ elif isinstance(cell, (int, float)):
+ dtype = 'numeric'
+ elif isinstance(cell, (datetime.datetime, datetime.date)):
+ dtype = 'datetime'
+ cell = self._shared_date.datetime_to_julian(cell)
+ attributes['s'] = STYLES[dtype]['style']
+ elif cell and cell[0] == '=':
+ dtype = 'formula'
+ else:
+ dtype = 'string'
+ cell = self._string_builder.add(cell)
+
+ attributes['t'] = STYLES[dtype]['type']
+
+ start_tag(doc, 'c', attributes)
+
+ if dtype == 'formula':
+ tag(doc, 'f', body = '%s' % cell[1:])
+ tag(doc, 'v')
+ else:
+ tag(doc, 'v', body = '%s' % cell)
+
+ end_tag(doc, 'c')
+
+
+ end_tag(doc, 'row')
+
+
+def save_dump(workbook, filename):
+
+ writer = ExcelDumpWriter(workbook)
+ writer.save(filename)
+ return True
+
+class ExcelDumpWriter(ExcelWriter):
+
+ def __init__(self, workbook):
+
+ self.workbook = workbook
+ self.style_writer = StyleDumpWriter(workbook)
+ self.style_writer._style_list.append(DATETIME_STYLE)
+
+ def _write_string_table(self, archive):
+
+ shared_string_table = self.workbook.strings_table_builder.get_table()
+ archive.writestr(ARC_SHARED_STRINGS,
+ write_string_table(shared_string_table))
+
+ return shared_string_table
+
+ def _write_worksheets(self, archive, shared_string_table, style_writer):
+
+ for i, sheet in enumerate(self.workbook.worksheets):
+ sheet.write_header()
+ sheet.close()
+ archive.write(sheet.filename, PACKAGE_WORKSHEETS + '/sheet%d.xml' % (i + 1))
+ os.remove(sheet.filename)
+
+
+class StyleDumpWriter(StyleWriter):
+
+ def _get_style_list(self, workbook):
+ return []
+
diff --git a/tablib/packages/openpyxl3/writer/excel.py b/tablib/packages/openpyxl3/writer/excel.py
new file mode 100644
index 0000000..a19666d
--- /dev/null
+++ b/tablib/packages/openpyxl3/writer/excel.py
@@ -0,0 +1,156 @@
+# file openpyxl/writer/excel.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write a .xlsx file."""
+
+# Python stdlib imports
+from zipfile import ZipFile, ZIP_DEFLATED
+from io import StringIO
+
+# package imports
+from ..shared.ooxml import ARC_SHARED_STRINGS, ARC_CONTENT_TYPES, \
+ ARC_ROOT_RELS, ARC_WORKBOOK_RELS, ARC_APP, ARC_CORE, ARC_THEME, \
+ ARC_STYLE, ARC_WORKBOOK, \
+ PACKAGE_WORKSHEETS, PACKAGE_DRAWINGS, PACKAGE_CHARTS
+from .strings import create_string_table, write_string_table
+from .workbook import write_content_types, write_root_rels, \
+ write_workbook_rels, write_properties_app, write_properties_core, \
+ write_workbook
+from .theme import write_theme
+from .styles import StyleWriter
+from .drawings import DrawingWriter, ShapeWriter
+from .charts import ChartWriter
+from .worksheet import write_worksheet, write_worksheet_rels
+
+
+class ExcelWriter(object):
+ """Write a workbook object to an Excel file."""
+
+ def __init__(self, workbook):
+ self.workbook = workbook
+ self.style_writer = StyleWriter(self.workbook)
+
+ def write_data(self, archive):
+ """Write the various xml files into the zip archive."""
+ # cleanup all worksheets
+ shared_string_table = self._write_string_table(archive)
+
+ archive.writestr(ARC_CONTENT_TYPES, write_content_types(self.workbook))
+ archive.writestr(ARC_ROOT_RELS, write_root_rels(self.workbook))
+ archive.writestr(ARC_WORKBOOK_RELS, write_workbook_rels(self.workbook))
+ archive.writestr(ARC_APP, write_properties_app(self.workbook))
+ archive.writestr(ARC_CORE,
+ write_properties_core(self.workbook.properties))
+ archive.writestr(ARC_THEME, write_theme())
+ archive.writestr(ARC_STYLE, self.style_writer.write_table())
+ archive.writestr(ARC_WORKBOOK, write_workbook(self.workbook))
+
+ self._write_worksheets(archive, shared_string_table, self.style_writer)
+
+ def _write_string_table(self, archive):
+
+ for ws in self.workbook.worksheets:
+ ws.garbage_collect()
+ shared_string_table = create_string_table(self.workbook)
+ archive.writestr(ARC_SHARED_STRINGS,
+ write_string_table(shared_string_table))
+
+ return shared_string_table
+
+ def _write_worksheets(self, archive, shared_string_table, style_writer):
+
+ drawing_id = 1
+ chart_id = 1
+ shape_id = 1
+
+ for i, sheet in enumerate(self.workbook.worksheets):
+ archive.writestr(PACKAGE_WORKSHEETS + '/sheet%d.xml' % (i + 1),
+ write_worksheet(sheet, shared_string_table,
+ style_writer.get_style_by_hash()))
+ if sheet._charts or sheet.relationships:
+ archive.writestr(PACKAGE_WORKSHEETS +
+ '/_rels/sheet%d.xml.rels' % (i + 1),
+ write_worksheet_rels(sheet, drawing_id))
+ if sheet._charts:
+ dw = DrawingWriter(sheet)
+ archive.writestr(PACKAGE_DRAWINGS + '/drawing%d.xml' % drawing_id,
+ dw.write())
+ archive.writestr(PACKAGE_DRAWINGS + '/_rels/drawing%d.xml.rels' % drawing_id,
+ dw.write_rels(chart_id))
+ drawing_id += 1
+
+ for chart in sheet._charts:
+ cw = ChartWriter(chart)
+ archive.writestr(PACKAGE_CHARTS + '/chart%d.xml' % chart_id,
+ cw.write())
+
+ if chart._shapes:
+ archive.writestr(PACKAGE_CHARTS + '/_rels/chart%d.xml.rels' % chart_id,
+ cw.write_rels(drawing_id))
+ sw = ShapeWriter(chart._shapes)
+ archive.writestr(PACKAGE_DRAWINGS + '/drawing%d.xml' % drawing_id,
+ sw.write(shape_id))
+ shape_id += len(chart._shapes)
+ drawing_id += 1
+
+ chart_id += 1
+
+
+ def save(self, filename):
+ """Write data into the archive."""
+ archive = ZipFile(filename, 'w', ZIP_DEFLATED)
+ self.write_data(archive)
+ archive.close()
+
+
+def save_workbook(workbook, filename):
+ """Save the given workbook on the filesystem under the name filename.
+
+ :param workbook: the workbook to save
+ :type workbook: :class:`openpyxl.workbook.Workbook`
+
+ :param filename: the path to which save the workbook
+ :type filename: string
+
+ :rtype: bool
+
+ """
+ writer = ExcelWriter(workbook)
+ writer.save(filename)
+ return True
+
+
+def save_virtual_workbook(workbook):
+ """Return an in-memory workbook, suitable for a Django response."""
+ writer = ExcelWriter(workbook)
+ temp_buffer = StringIO()
+ try:
+ archive = ZipFile(temp_buffer, 'w', ZIP_DEFLATED)
+ writer.write_data(archive)
+ finally:
+ archive.close()
+ virtual_workbook = temp_buffer.getvalue()
+ temp_buffer.close()
+ return virtual_workbook
diff --git a/tablib/packages/openpyxl3/writer/strings.py b/tablib/packages/openpyxl3/writer/strings.py
new file mode 100644
index 0000000..706c2b6
--- /dev/null
+++ b/tablib/packages/openpyxl3/writer/strings.py
@@ -0,0 +1,86 @@
+# file openpyxl/writer/strings.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write the shared string table."""
+
+# Python stdlib imports
+from io import StringIO
+
+# package imports
+from ..shared.xmltools import start_tag, end_tag, tag, XMLGenerator
+
+
+def create_string_table(workbook):
+ """Compile the string table for a workbook."""
+ strings = set()
+ for sheet in workbook.worksheets:
+ for cell in sheet.get_cell_collection():
+ if cell.data_type == cell.TYPE_STRING and cell._value is not None:
+ strings.add(cell.value)
+ return dict((key, i) for i, key in enumerate(strings))
+
+
+def write_string_table(string_table):
+ """Write the string table xml."""
+ temp_buffer = StringIO()
+ doc = XMLGenerator(temp_buffer, 'utf-8')
+ start_tag(doc, 'sst', {'xmlns':
+ 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
+ 'uniqueCount': '%d' % len(string_table)})
+ strings_to_write = sorted(iter(string_table.items()),
+ key=lambda pair: pair[1])
+ for key in [pair[0] for pair in strings_to_write]:
+ start_tag(doc, 'si')
+ if key.strip() != key:
+ attr = {'xml:space': 'preserve'}
+ else:
+ attr = {}
+ tag(doc, 't', attr, key)
+ end_tag(doc, 'si')
+ end_tag(doc, 'sst')
+ string_table_xml = temp_buffer.getvalue()
+ temp_buffer.close()
+ return string_table_xml
+
+class StringTableBuilder(object):
+
+ def __init__(self):
+
+ self.counter = 0
+ self.dct = {}
+
+ def add(self, key):
+
+ key = key.strip()
+ try:
+ return self.dct[key]
+ except KeyError:
+ res = self.dct[key] = self.counter
+ self.counter += 1
+ return res
+
+ def get_table(self):
+
+ return self.dct
diff --git a/tablib/packages/openpyxl3/writer/styles.py b/tablib/packages/openpyxl3/writer/styles.py
new file mode 100644
index 0000000..3d73382
--- /dev/null
+++ b/tablib/packages/openpyxl3/writer/styles.py
@@ -0,0 +1,256 @@
+# file openpyxl/writer/styles.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write the shared style table."""
+
+# package imports
+from ..shared.xmltools import Element, SubElement
+from ..shared.xmltools import get_document_content
+from .. import style
+
+class StyleWriter(object):
+
+ def __init__(self, workbook):
+ self._style_list = self._get_style_list(workbook)
+ self._root = Element('styleSheet',
+ {'xmlns':'http://schemas.openxmlformats.org/spreadsheetml/2006/main'})
+
+ def _get_style_list(self, workbook):
+ crc = {}
+ for worksheet in workbook.worksheets:
+ for style in list(worksheet._styles.values()):
+ crc[hash(style)] = style
+ self.style_table = dict([(style, i+1) \
+ for i, style in enumerate(list(crc.values()))])
+ sorted_styles = sorted(iter(self.style_table.items()), \
+ key = lambda pair:pair[1])
+ return [s[0] for s in sorted_styles]
+
+ def get_style_by_hash(self):
+ return dict([(hash(style), id) \
+ for style, id in self.style_table.items()])
+
+ def write_table(self):
+ number_format_table = self._write_number_formats()
+ fonts_table = self._write_fonts()
+ fills_table = self._write_fills()
+ borders_table = self._write_borders()
+ self._write_cell_style_xfs()
+ self._write_cell_xfs(number_format_table, fonts_table, fills_table, borders_table)
+ self._write_cell_style()
+ self._write_dxfs()
+ self._write_table_styles()
+
+ return get_document_content(xml_node=self._root)
+
+ def _write_fonts(self):
+ """ add fonts part to root
+ return {font.crc => index}
+ """
+
+ fonts = SubElement(self._root, 'fonts')
+
+ # default
+ font_node = SubElement(fonts, 'font')
+ SubElement(font_node, 'sz', {'val':'11'})
+ SubElement(font_node, 'color', {'theme':'1'})
+ SubElement(font_node, 'name', {'val':'Calibri'})
+ SubElement(font_node, 'family', {'val':'2'})
+ SubElement(font_node, 'scheme', {'val':'minor'})
+
+ # others
+ table = {}
+ index = 1
+ for st in self._style_list:
+ if hash(st.font) != hash(style.DEFAULTS.font) and hash(st.font) not in table:
+ table[hash(st.font)] = str(index)
+ font_node = SubElement(fonts, 'font')
+ SubElement(font_node, 'sz', {'val':str(st.font.size)})
+ SubElement(font_node, 'color', {'rgb':str(st.font.color.index)})
+ SubElement(font_node, 'name', {'val':st.font.name})
+ SubElement(font_node, 'family', {'val':'2'})
+ SubElement(font_node, 'scheme', {'val':'minor'})
+ if st.font.bold:
+ SubElement(font_node, 'b')
+ if st.font.italic:
+ SubElement(font_node, 'i')
+ index += 1
+
+ fonts.attrib["count"] = str(index)
+ return table
+
+ def _write_fills(self):
+ fills = SubElement(self._root, 'fills', {'count':'2'})
+ fill = SubElement(fills, 'fill')
+ SubElement(fill, 'patternFill', {'patternType':'none'})
+ fill = SubElement(fills, 'fill')
+ SubElement(fill, 'patternFill', {'patternType':'gray125'})
+
+ table = {}
+ index = 2
+ for st in self._style_list:
+ if hash(st.fill) != hash(style.DEFAULTS.fill) and hash(st.fill) not in table:
+ table[hash(st.fill)] = str(index)
+ fill = SubElement(fills, 'fill')
+ if hash(st.fill.fill_type) != hash(style.DEFAULTS.fill.fill_type):
+ node = SubElement(fill,'patternFill', {'patternType':st.fill.fill_type})
+ if hash(st.fill.start_color) != hash(style.DEFAULTS.fill.start_color):
+
+ SubElement(node, 'fgColor', {'rgb':str(st.fill.start_color.index)})
+ if hash(st.fill.end_color) != hash(style.DEFAULTS.fill.end_color):
+ SubElement(node, 'bgColor', {'rgb':str(st.fill.start_color.index)})
+ index += 1
+
+ fills.attrib["count"] = str(index)
+ return table
+
+ def _write_borders(self):
+ borders = SubElement(self._root, 'borders')
+
+ # default
+ border = SubElement(borders, 'border')
+ SubElement(border, 'left')
+ SubElement(border, 'right')
+ SubElement(border, 'top')
+ SubElement(border, 'bottom')
+ SubElement(border, 'diagonal')
+
+ # others
+ table = {}
+ index = 1
+ for st in self._style_list:
+ if hash(st.borders) != hash(style.DEFAULTS.borders) and hash(st.borders) not in table:
+ table[hash(st.borders)] = str(index)
+ border = SubElement(borders, 'border')
+ # caution: respect this order
+ for side in ('left','right','top','bottom','diagonal'):
+ obj = getattr(st.borders, side)
+ node = SubElement(border, side, {'style':obj.border_style})
+ SubElement(node, 'color', {'rgb':str(obj.color.index)})
+ index += 1
+
+ borders.attrib["count"] = str(index)
+ return table
+
+ def _write_cell_style_xfs(self):
+ cell_style_xfs = SubElement(self._root, 'cellStyleXfs', {'count':'1'})
+ xf = SubElement(cell_style_xfs, 'xf',
+ {'numFmtId':"0", 'fontId':"0", 'fillId':"0", 'borderId':"0"})
+
+ def _write_cell_xfs(self, number_format_table, fonts_table, fills_table, borders_table):
+ """ write styles combinations based on ids found in tables """
+
+ # writing the cellXfs
+ cell_xfs = SubElement(self._root, 'cellXfs',
+ {'count':'%d' % (len(self._style_list) + 1)})
+
+ # default
+ def _get_default_vals():
+ return dict(numFmtId='0', fontId='0', fillId='0',
+ xfId='0', borderId='0')
+
+ SubElement(cell_xfs, 'xf', _get_default_vals())
+
+ for st in self._style_list:
+ vals = _get_default_vals()
+
+ if hash(st.font) != hash(style.DEFAULTS.font):
+ vals['fontId'] = fonts_table[hash(st.font)]
+ vals['applyFont'] = '1'
+
+ if hash(st.borders) != hash(style.DEFAULTS.borders):
+ vals['borderId'] = borders_table[hash(st.borders)]
+ vals['applyBorder'] = '1'
+
+ if hash(st.fill) != hash(style.DEFAULTS.fill):
+ vals['fillId'] = fills_table[hash(st.fill)]
+ vals['applyFillId'] = '1'
+
+ if st.number_format != style.DEFAULTS.number_format:
+ vals['numFmtId'] = '%d' % number_format_table[st.number_format]
+ vals['applyNumberFormat'] = '1'
+
+ if hash(st.alignment) != hash(style.DEFAULTS.alignment):
+ vals['applyAlignment'] = '1'
+
+ node = SubElement(cell_xfs, 'xf', vals)
+
+ if hash(st.alignment) != hash(style.DEFAULTS.alignment):
+ alignments = {}
+
+ for align_attr in ['horizontal','vertical']:
+ if hash(getattr(st.alignment, align_attr)) != hash(getattr(style.DEFAULTS.alignment, align_attr)):
+ alignments[align_attr] = getattr(st.alignment, align_attr)
+
+ SubElement(node, 'alignment', alignments)
+
+
+ def _write_cell_style(self):
+ cell_styles = SubElement(self._root, 'cellStyles', {'count':'1'})
+ cell_style = SubElement(cell_styles, 'cellStyle',
+ {'name':"Normal", 'xfId':"0", 'builtinId':"0"})
+
+ def _write_dxfs(self):
+ dxfs = SubElement(self._root, 'dxfs', {'count':'0'})
+
+ def _write_table_styles(self):
+
+ table_styles = SubElement(self._root, 'tableStyles',
+ {'count':'0', 'defaultTableStyle':'TableStyleMedium9',
+ 'defaultPivotStyle':'PivotStyleLight16'})
+
+ def _write_number_formats(self):
+
+ number_format_table = {}
+
+ number_format_list = []
+ exceptions_list = []
+ num_fmt_id = 165 # start at a greatly higher value as any builtin can go
+ num_fmt_offset = 0
+
+ for style in self._style_list:
+
+ if not style.number_format in number_format_list :
+ number_format_list.append(style.number_format)
+
+ for number_format in number_format_list:
+
+ if number_format.is_builtin():
+ btin = number_format.builtin_format_id(number_format.format_code)
+ number_format_table[number_format] = btin
+ else:
+ number_format_table[number_format] = num_fmt_id + num_fmt_offset
+ num_fmt_offset += 1
+ exceptions_list.append(number_format)
+
+ num_fmts = SubElement(self._root, 'numFmts',
+ {'count':'%d' % len(exceptions_list)})
+
+ for number_format in exceptions_list :
+ SubElement(num_fmts, 'numFmt',
+ {'numFmtId':'%d' % number_format_table[number_format],
+ 'formatCode':'%s' % number_format.format_code})
+
+ return number_format_table
diff --git a/tablib/packages/openpyxl3/writer/theme.py b/tablib/packages/openpyxl3/writer/theme.py
new file mode 100644
index 0000000..80700f2
--- /dev/null
+++ b/tablib/packages/openpyxl3/writer/theme.py
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+# file openpyxl/writer/theme.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write the theme xml based on a fixed string."""
+
+# package imports
+from ..shared.xmltools import fromstring, get_document_content
+
+
+def write_theme():
+ """Write the theme xml."""
+ xml_node = fromstring(
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
+
+ '<a:theme xmlns:a="http://schemas.openxmlformats.org/'
+ 'drawingml/2006/main" name="Office Theme">'
+ '<a:themeElements>'
+
+ '<a:clrScheme name="Office">'
+ '<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>'
+ '<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>'
+ '<a:dk2><a:srgbClr val="1F497D"/></a:dk2>'
+ '<a:lt2><a:srgbClr val="EEECE1"/></a:lt2>'
+ '<a:accent1><a:srgbClr val="4F81BD"/></a:accent1>'
+ '<a:accent2><a:srgbClr val="C0504D"/></a:accent2>'
+ '<a:accent3><a:srgbClr val="9BBB59"/></a:accent3>'
+ '<a:accent4><a:srgbClr val="8064A2"/></a:accent4>'
+ '<a:accent5><a:srgbClr val="4BACC6"/></a:accent5>'
+ '<a:accent6><a:srgbClr val="F79646"/></a:accent6>'
+ '<a:hlink><a:srgbClr val="0000FF"/></a:hlink>'
+ '<a:folHlink><a:srgbClr val="800080"/></a:folHlink>'
+ '</a:clrScheme>'
+
+ '<a:fontScheme name="Office">'
+ '<a:majorFont>'
+ '<a:latin typeface="Cambria"/>'
+ '<a:ea typeface=""/>'
+ '<a:cs typeface=""/>'
+ '<a:font script="Jpan" typeface="MS Pゴシック"/>'
+ '<a:font script="Hang" typeface="맑은 고딕"/>'
+ '<a:font script="Hans" typeface="宋体"/>'
+ '<a:font script="Hant" typeface="新細明體"/>'
+ '<a:font script="Arab" typeface="Times New Roman"/>'
+ '<a:font script="Hebr" typeface="Times New Roman"/>'
+ '<a:font script="Thai" typeface="Tahoma"/>'
+ '<a:font script="Ethi" typeface="Nyala"/>'
+ '<a:font script="Beng" typeface="Vrinda"/>'
+ '<a:font script="Gujr" typeface="Shruti"/>'
+ '<a:font script="Khmr" typeface="MoolBoran"/>'
+ '<a:font script="Knda" typeface="Tunga"/>'
+ '<a:font script="Guru" typeface="Raavi"/>'
+ '<a:font script="Cans" typeface="Euphemia"/>'
+ '<a:font script="Cher" typeface="Plantagenet Cherokee"/>'
+ '<a:font script="Yiii" typeface="Microsoft Yi Baiti"/>'
+ '<a:font script="Tibt" typeface="Microsoft Himalaya"/>'
+ '<a:font script="Thaa" typeface="MV Boli"/>'
+ '<a:font script="Deva" typeface="Mangal"/>'
+ '<a:font script="Telu" typeface="Gautami"/>'
+ '<a:font script="Taml" typeface="Latha"/>'
+ '<a:font script="Syrc" typeface="Estrangelo Edessa"/>'
+ '<a:font script="Orya" typeface="Kalinga"/>'
+ '<a:font script="Mlym" typeface="Kartika"/>'
+ '<a:font script="Laoo" typeface="DokChampa"/>'
+ '<a:font script="Sinh" typeface="Iskoola Pota"/>'
+ '<a:font script="Mong" typeface="Mongolian Baiti"/>'
+ '<a:font script="Viet" typeface="Times New Roman"/>'
+ '<a:font script="Uigh" typeface="Microsoft Uighur"/>'
+ '</a:majorFont>'
+ '<a:minorFont>'
+ '<a:latin typeface="Calibri"/>'
+ '<a:ea typeface=""/>'
+ '<a:cs typeface=""/>'
+ '<a:font script="Jpan" typeface="MS Pゴシック"/>'
+ '<a:font script="Hang" typeface="맑은 고딕"/>'
+ '<a:font script="Hans" typeface="宋体"/>'
+ '<a:font script="Hant" typeface="新細明體"/>'
+ '<a:font script="Arab" typeface="Arial"/>'
+ '<a:font script="Hebr" typeface="Arial"/>'
+ '<a:font script="Thai" typeface="Tahoma"/>'
+ '<a:font script="Ethi" typeface="Nyala"/>'
+ '<a:font script="Beng" typeface="Vrinda"/>'
+ '<a:font script="Gujr" typeface="Shruti"/>'
+ '<a:font script="Khmr" typeface="DaunPenh"/>'
+ '<a:font script="Knda" typeface="Tunga"/>'
+ '<a:font script="Guru" typeface="Raavi"/>'
+ '<a:font script="Cans" typeface="Euphemia"/>'
+ '<a:font script="Cher" typeface="Plantagenet Cherokee"/>'
+ '<a:font script="Yiii" typeface="Microsoft Yi Baiti"/>'
+ '<a:font script="Tibt" typeface="Microsoft Himalaya"/>'
+ '<a:font script="Thaa" typeface="MV Boli"/>'
+ '<a:font script="Deva" typeface="Mangal"/>'
+ '<a:font script="Telu" typeface="Gautami"/>'
+ '<a:font script="Taml" typeface="Latha"/>'
+ '<a:font script="Syrc" typeface="Estrangelo Edessa"/>'
+ '<a:font script="Orya" typeface="Kalinga"/>'
+ '<a:font script="Mlym" typeface="Kartika"/>'
+ '<a:font script="Laoo" typeface="DokChampa"/>'
+ '<a:font script="Sinh" typeface="Iskoola Pota"/>'
+ '<a:font script="Mong" typeface="Mongolian Baiti"/>'
+ '<a:font script="Viet" typeface="Arial"/>'
+ '<a:font script="Uigh" typeface="Microsoft Uighur"/>'
+ '</a:minorFont>'
+ '</a:fontScheme>'
+
+ '<a:fmtScheme name="Office">'
+ '<a:fillStyleLst>'
+ '<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
+ '<a:gradFill rotWithShape="1"><a:gsLst>'
+ '<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="50000"/>'
+ '<a:satMod val="300000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="35000"><a:schemeClr val="phClr"><a:tint val="37000"/>'
+ '<a:satMod val="300000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="100000"><a:schemeClr val="phClr"><a:tint val="15000"/>'
+ '<a:satMod val="350000"/></a:schemeClr></a:gs></a:gsLst>'
+ '<a:lin ang="16200000" scaled="1"/></a:gradFill>'
+ '<a:gradFill rotWithShape="1"><a:gsLst>'
+ '<a:gs pos="0"><a:schemeClr val="phClr"><a:shade val="51000"/>'
+ '<a:satMod val="130000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="80000"><a:schemeClr val="phClr"><a:shade val="93000"/>'
+ '<a:satMod val="130000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="100000"><a:schemeClr val="phClr">'
+ '<a:shade val="94000"/>'
+ '<a:satMod val="135000"/></a:schemeClr></a:gs></a:gsLst>'
+ '<a:lin ang="16200000" scaled="0"/></a:gradFill></a:fillStyleLst>'
+ '<a:lnStyleLst>'
+ '<a:ln w="9525" cap="flat" cmpd="sng" algn="ctr">'
+ '<a:solidFill><a:schemeClr val="phClr"><a:shade val="95000"/>'
+ '<a:satMod val="105000"/></a:schemeClr></a:solidFill>'
+ '<a:prstDash val="solid"/></a:ln>'
+ '<a:ln w="25400" cap="flat" cmpd="sng" algn="ctr"><a:solidFill>'
+ '<a:schemeClr val="phClr"/></a:solidFill>'
+ '<a:prstDash val="solid"/></a:ln>'
+ '<a:ln w="38100" cap="flat" cmpd="sng" algn="ctr"><a:solidFill>'
+ '<a:schemeClr val="phClr"/></a:solidFill>'
+ '<a:prstDash val="solid"/></a:ln></a:lnStyleLst>'
+ '<a:effectStyleLst><a:effectStyle><a:effectLst>'
+ '<a:outerShdw blurRad="40000" dist="20000" dir="5400000" '
+ 'rotWithShape="0"><a:srgbClr val="000000">'
+ '<a:alpha val="38000"/></a:srgbClr></a:outerShdw></a:effectLst>'
+ '</a:effectStyle><a:effectStyle><a:effectLst>'
+ '<a:outerShdw blurRad="40000" dist="23000" dir="5400000" '
+ 'rotWithShape="0"><a:srgbClr val="000000">'
+ '<a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst>'
+ '</a:effectStyle><a:effectStyle><a:effectLst>'
+ '<a:outerShdw blurRad="40000" dist="23000" dir="5400000" '
+ 'rotWithShape="0"><a:srgbClr val="000000">'
+ '<a:alpha val="35000"/></a:srgbClr></a:outerShdw></a:effectLst>'
+ '<a:scene3d><a:camera prst="orthographicFront">'
+ '<a:rot lat="0" lon="0" rev="0"/></a:camera>'
+ '<a:lightRig rig="threePt" dir="t">'
+ '<a:rot lat="0" lon="0" rev="1200000"/></a:lightRig>'
+ '</a:scene3d><a:sp3d><a:bevelT w="63500" h="25400"/>'
+ '</a:sp3d></a:effectStyle></a:effectStyleLst>'
+ '<a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/>'
+ '</a:solidFill><a:gradFill rotWithShape="1"><a:gsLst>'
+ '<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="40000"/>'
+ '<a:satMod val="350000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="40000"><a:schemeClr val="phClr"><a:tint val="45000"/>'
+ '<a:shade val="99000"/><a:satMod val="350000"/>'
+ '</a:schemeClr></a:gs>'
+ '<a:gs pos="100000"><a:schemeClr val="phClr">'
+ '<a:shade val="20000"/><a:satMod val="255000"/>'
+ '</a:schemeClr></a:gs></a:gsLst>'
+ '<a:path path="circle">'
+ '<a:fillToRect l="50000" t="-80000" r="50000" b="180000"/>'
+ '</a:path>'
+ '</a:gradFill><a:gradFill rotWithShape="1"><a:gsLst>'
+ '<a:gs pos="0"><a:schemeClr val="phClr"><a:tint val="80000"/>'
+ '<a:satMod val="300000"/></a:schemeClr></a:gs>'
+ '<a:gs pos="100000"><a:schemeClr val="phClr">'
+ '<a:shade val="30000"/><a:satMod val="200000"/>'
+ '</a:schemeClr></a:gs></a:gsLst>'
+ '<a:path path="circle">'
+ '<a:fillToRect l="50000" t="50000" r="50000" b="50000"/></a:path>'
+ '</a:gradFill></a:bgFillStyleLst></a:fmtScheme>'
+ '</a:themeElements>'
+ '<a:objectDefaults/><a:extraClrSchemeLst/>'
+ '</a:theme>')
+ return get_document_content(xml_node)
diff --git a/tablib/packages/openpyxl3/writer/workbook.py b/tablib/packages/openpyxl3/writer/workbook.py
new file mode 100644
index 0000000..e7b390c
--- /dev/null
+++ b/tablib/packages/openpyxl3/writer/workbook.py
@@ -0,0 +1,204 @@
+# file openpyxl/writer/workbook.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write the workbook global settings to the archive."""
+
+# package imports
+from ..shared.xmltools import Element, SubElement
+from ..cell import absolute_coordinate
+from ..shared.xmltools import get_document_content
+from ..shared.ooxml import NAMESPACES, ARC_CORE, ARC_WORKBOOK, \
+ ARC_APP, ARC_THEME, ARC_STYLE, ARC_SHARED_STRINGS
+from ..shared.date_time import datetime_to_W3CDTF
+
+
+def write_properties_core(properties):
+ """Write the core properties to xml."""
+ root = Element('cp:coreProperties', {'xmlns:cp': NAMESPACES['cp'],
+ 'xmlns:xsi': NAMESPACES['xsi'], 'xmlns:dc': NAMESPACES['dc'],
+ 'xmlns:dcterms': NAMESPACES['dcterms'],
+ 'xmlns:dcmitype': NAMESPACES['dcmitype'], })
+ SubElement(root, 'dc:creator').text = properties.creator
+ SubElement(root, 'cp:lastModifiedBy').text = properties.last_modified_by
+ SubElement(root, 'dcterms:created', \
+ {'xsi:type': 'dcterms:W3CDTF'}).text = \
+ datetime_to_W3CDTF(properties.created)
+ SubElement(root, 'dcterms:modified',
+ {'xsi:type': 'dcterms:W3CDTF'}).text = \
+ datetime_to_W3CDTF(properties.modified)
+ return get_document_content(root)
+
+
+def write_content_types(workbook):
+ """Write the content-types xml."""
+ root = Element('Types', {'xmlns': 'http://schemas.openxmlformats.org/package/2006/content-types'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_THEME, 'ContentType': 'application/vnd.openxmlformats-officedocument.theme+xml'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_STYLE, 'ContentType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml'})
+ SubElement(root, 'Default', {'Extension': 'rels', 'ContentType': 'application/vnd.openxmlformats-package.relationships+xml'})
+ SubElement(root, 'Default', {'Extension': 'xml', 'ContentType': 'application/xml'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_WORKBOOK, 'ContentType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_APP, 'ContentType': 'application/vnd.openxmlformats-officedocument.extended-properties+xml'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_CORE, 'ContentType': 'application/vnd.openxmlformats-package.core-properties+xml'})
+ SubElement(root, 'Override', {'PartName': '/' + ARC_SHARED_STRINGS, 'ContentType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'})
+
+ drawing_id = 1
+ chart_id = 1
+
+ for sheet_id, sheet in enumerate(workbook.worksheets):
+ SubElement(root, 'Override',
+ {'PartName': '/xl/worksheets/sheet%d.xml' % (sheet_id + 1),
+ 'ContentType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'})
+ if sheet._charts:
+ SubElement(root, 'Override',
+ {'PartName' : '/xl/drawings/drawing%d.xml' % (sheet_id + 1),
+ 'ContentType' : 'application/vnd.openxmlformats-officedocument.drawing+xml'})
+ drawing_id += 1
+
+ for chart in sheet._charts:
+ SubElement(root, 'Override',
+ {'PartName' : '/xl/charts/chart%d.xml' % chart_id,
+ 'ContentType' : 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml'})
+ chart_id += 1
+ if chart._shapes:
+ SubElement(root, 'Override',
+ {'PartName' : '/xl/drawings/drawing%d.xml' % drawing_id,
+ 'ContentType' : 'application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml'})
+ drawing_id += 1
+
+ return get_document_content(root)
+
+
+def write_properties_app(workbook):
+ """Write the properties xml."""
+ worksheets_count = len(workbook.worksheets)
+ root = Element('Properties', {'xmlns': 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
+ 'xmlns:vt': 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'})
+ SubElement(root, 'Application').text = 'Microsoft Excel'
+ SubElement(root, 'DocSecurity').text = '0'
+ SubElement(root, 'ScaleCrop').text = 'false'
+ SubElement(root, 'Company')
+ SubElement(root, 'LinksUpToDate').text = 'false'
+ SubElement(root, 'SharedDoc').text = 'false'
+ SubElement(root, 'HyperlinksChanged').text = 'false'
+ SubElement(root, 'AppVersion').text = '12.0000'
+
+ # heading pairs part
+ heading_pairs = SubElement(root, 'HeadingPairs')
+ vector = SubElement(heading_pairs, 'vt:vector',
+ {'size': '2', 'baseType': 'variant'})
+ variant = SubElement(vector, 'vt:variant')
+ SubElement(variant, 'vt:lpstr').text = 'Worksheets'
+ variant = SubElement(vector, 'vt:variant')
+ SubElement(variant, 'vt:i4').text = '%d' % worksheets_count
+
+ # title of parts
+ title_of_parts = SubElement(root, 'TitlesOfParts')
+ vector = SubElement(title_of_parts, 'vt:vector',
+ {'size': '%d' % worksheets_count, 'baseType': 'lpstr'})
+ for ws in workbook.worksheets:
+ SubElement(vector, 'vt:lpstr').text = '%s' % ws.title
+ return get_document_content(root)
+
+
+def write_root_rels(workbook):
+ """Write the relationships xml."""
+ root = Element('Relationships', {'xmlns':
+ 'http://schemas.openxmlformats.org/package/2006/relationships'})
+ SubElement(root, 'Relationship', {'Id': 'rId1', 'Target': ARC_WORKBOOK,
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument'})
+ SubElement(root, 'Relationship', {'Id': 'rId2', 'Target': ARC_CORE,
+ 'Type': 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties'})
+ SubElement(root, 'Relationship', {'Id': 'rId3', 'Target': ARC_APP,
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties'})
+ return get_document_content(root)
+
+
+def write_workbook(workbook):
+ """Write the core workbook xml."""
+ root = Element('workbook', {'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
+ 'xml:space': 'preserve', 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'})
+ SubElement(root, 'fileVersion', {'appName': 'xl', 'lastEdited': '4',
+ 'lowestEdited': '4', 'rupBuild': '4505'})
+ SubElement(root, 'workbookPr', {'defaultThemeVersion': '124226',
+ 'codeName': 'ThisWorkbook'})
+ book_views = SubElement(root, 'bookViews')
+ SubElement(book_views, 'workbookView', {'activeTab': '%d' % workbook.get_index(workbook.get_active_sheet()),
+ 'autoFilterDateGrouping': '1', 'firstSheet': '0', 'minimized': '0',
+ 'showHorizontalScroll': '1', 'showSheetTabs': '1',
+ 'showVerticalScroll': '1', 'tabRatio': '600',
+ 'visibility': 'visible'})
+ # worksheets
+ sheets = SubElement(root, 'sheets')
+ for i, sheet in enumerate(workbook.worksheets):
+ sheet_node = SubElement(sheets, 'sheet', {'name': sheet.title,
+ 'sheetId': '%d' % (i + 1), 'r:id': 'rId%d' % (i + 1)})
+ if not sheet.sheet_state == sheet.SHEETSTATE_VISIBLE:
+ sheet_node.set('state', sheet.sheet_state)
+ # named ranges
+ defined_names = SubElement(root, 'definedNames')
+ for named_range in workbook.get_named_ranges():
+ name = SubElement(defined_names, 'definedName',
+ {'name': named_range.name})
+
+ # as there can be many cells in one range, generate the list of ranges
+ dest_cells = []
+ cell_ids = []
+ for worksheet, range_name in named_range.destinations:
+ cell_ids.append(workbook.get_index(worksheet))
+ dest_cells.append("'%s'!%s" % (worksheet.title.replace("'", "''"),
+ absolute_coordinate(range_name)))
+
+ # for local ranges, we must check all the cells belong to the same sheet
+ base_id = cell_ids[0]
+ if named_range.local_only and all([x == base_id for x in cell_ids]):
+ name.set('localSheetId', '%s' % base_id)
+
+ # finally write the cells list
+ name.text = ','.join(dest_cells)
+
+ SubElement(root, 'calcPr', {'calcId': '124519', 'calcMode': 'auto',
+ 'fullCalcOnLoad': '1'})
+ return get_document_content(root)
+
+
+def write_workbook_rels(workbook):
+ """Write the workbook relationships xml."""
+ root = Element('Relationships', {'xmlns':
+ 'http://schemas.openxmlformats.org/package/2006/relationships'})
+ for i in range(len(workbook.worksheets)):
+ SubElement(root, 'Relationship', {'Id': 'rId%d' % (i + 1),
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
+ 'Target': 'worksheets/sheet%s.xml' % (i + 1)})
+ rid = len(workbook.worksheets) + 1
+ SubElement(root, 'Relationship',
+ {'Id': 'rId%d' % rid, 'Target': 'sharedStrings.xml',
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings'})
+ SubElement(root, 'Relationship',
+ {'Id': 'rId%d' % (rid + 1), 'Target': 'styles.xml',
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'})
+ SubElement(root, 'Relationship',
+ {'Id': 'rId%d' % (rid + 2), 'Target': 'theme/theme1.xml',
+ 'Type': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme'})
+ return get_document_content(root)
diff --git a/tablib/packages/openpyxl3/writer/worksheet.py b/tablib/packages/openpyxl3/writer/worksheet.py
new file mode 100644
index 0000000..21d9e9b
--- /dev/null
+++ b/tablib/packages/openpyxl3/writer/worksheet.py
@@ -0,0 +1,209 @@
+# file openpyxl/writer/worksheet.py
+
+# Copyright (c) 2010 openpyxl
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# @license: http://www.opensource.org/licenses/mit-license.php
+# @author: Eric Gazoni
+
+"""Write worksheets to xml representations."""
+
+# Python stdlib imports
+from io import StringIO # cStringIO doesn't handle unicode
+
+# package imports
+from ..cell import coordinate_from_string, column_index_from_string
+from ..shared.xmltools import Element, SubElement, XMLGenerator, \
+ get_document_content, start_tag, end_tag, tag
+
+
+def row_sort(cell):
+ """Translate column names for sorting."""
+ return column_index_from_string(cell.column)
+
+
+def write_worksheet(worksheet, string_table, style_table):
+ """Write a worksheet to an xml file."""
+ xml_file = StringIO()
+ doc = XMLGenerator(xml_file, 'utf-8')
+ start_tag(doc, 'worksheet',
+ {'xml:space': 'preserve',
+ 'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'})
+ start_tag(doc, 'sheetPr')
+ tag(doc, 'outlinePr',
+ {'summaryBelow': '%d' % (worksheet.show_summary_below),
+ 'summaryRight': '%d' % (worksheet.show_summary_right)})
+ end_tag(doc, 'sheetPr')
+ tag(doc, 'dimension', {'ref': '%s' % worksheet.calculate_dimension()})
+ write_worksheet_sheetviews(doc, worksheet)
+ tag(doc, 'sheetFormatPr', {'defaultRowHeight': '15'})
+ write_worksheet_cols(doc, worksheet)
+ write_worksheet_data(doc, worksheet, string_table, style_table)
+ if worksheet.auto_filter:
+ tag(doc, 'autoFilter', {'ref': worksheet.auto_filter})
+ write_worksheet_hyperlinks(doc, worksheet)
+ if worksheet._charts:
+ tag(doc, 'drawing', {'r:id':'rId1'})
+ end_tag(doc, 'worksheet')
+ doc.endDocument()
+ xml_string = xml_file.getvalue()
+ xml_file.close()
+ return xml_string
+
+def write_worksheet_sheetviews(doc, worksheet):
+ start_tag(doc, 'sheetViews')
+ start_tag(doc, 'sheetView', {'workbookViewId': '0'})
+ selectionAttrs = {}
+ topLeftCell = worksheet.freeze_panes
+ if topLeftCell:
+ colName, row = coordinate_from_string(topLeftCell)
+ column = column_index_from_string(colName)
+ pane = 'topRight'
+ paneAttrs = {}
+ if column > 1:
+ paneAttrs['xSplit'] = str(column - 1)
+ if row > 1:
+ paneAttrs['ySplit'] = str(row - 1)
+ pane = 'bottomLeft'
+ if column > 1:
+ pane = 'bottomRight'
+ paneAttrs.update(dict(topLeftCell=topLeftCell,
+ activePane=pane,
+ state='frozen'))
+ tag(doc, 'pane', paneAttrs)
+ selectionAttrs['pane'] = pane
+ if row > 1 and column > 1:
+ tag(doc, 'selection', {'pane': 'topRight'})
+ tag(doc, 'selection', {'pane': 'bottomLeft'})
+
+ selectionAttrs.update({'activeCell': worksheet.active_cell,
+ 'sqref': worksheet.selected_cell})
+
+ tag(doc, 'selection', selectionAttrs)
+ end_tag(doc, 'sheetView')
+ end_tag(doc, 'sheetViews')
+
+
+def write_worksheet_cols(doc, worksheet):
+ """Write worksheet columns to xml."""
+ if worksheet.column_dimensions:
+ start_tag(doc, 'cols')
+ for column_string, columndimension in \
+ worksheet.column_dimensions.items():
+ col_index = column_index_from_string(column_string)
+ col_def = {}
+ col_def['collapsed'] = str(columndimension.style_index)
+ col_def['min'] = str(col_index)
+ col_def['max'] = str(col_index)
+ if columndimension.width != \
+ worksheet.default_column_dimension.width:
+ col_def['customWidth'] = 'true'
+ if not columndimension.visible:
+ col_def['hidden'] = 'true'
+ if columndimension.outline_level > 0:
+ col_def['outlineLevel'] = str(columndimension.outline_level)
+ if columndimension.collapsed:
+ col_def['collapsed'] = 'true'
+ if columndimension.auto_size:
+ col_def['bestFit'] = 'true'
+ if columndimension.width > 0:
+ col_def['width'] = str(columndimension.width)
+ else:
+ col_def['width'] = '9.10'
+ tag(doc, 'col', col_def)
+ end_tag(doc, 'cols')
+
+
+def write_worksheet_data(doc, worksheet, string_table, style_table):
+ """Write worksheet data to xml."""
+ start_tag(doc, 'sheetData')
+ max_column = worksheet.get_highest_column()
+ style_id_by_hash = style_table
+ cells_by_row = {}
+ for cell in worksheet.get_cell_collection():
+ cells_by_row.setdefault(cell.row, []).append(cell)
+ for row_idx in sorted(cells_by_row):
+ row_dimension = worksheet.row_dimensions[row_idx]
+ attrs = {'r': '%d' % row_idx,
+ 'spans': '1:%d' % max_column}
+ if row_dimension.height > 0:
+ attrs['ht'] = str(row_dimension.height)
+ attrs['customHeight'] = '1'
+ start_tag(doc, 'row', attrs)
+ row_cells = cells_by_row[row_idx]
+ sorted_cells = sorted(row_cells, key = row_sort)
+ for cell in sorted_cells:
+ value = cell._value
+ coordinate = cell.get_coordinate()
+ attributes = {'r': coordinate}
+ attributes['t'] = cell.data_type
+ if coordinate in worksheet._styles:
+ attributes['s'] = '%d' % style_id_by_hash[
+ hash(worksheet._styles[coordinate])]
+ start_tag(doc, 'c', attributes)
+ if value is None:
+ tag(doc, 'v', body='')
+ elif cell.data_type == cell.TYPE_STRING:
+ tag(doc, 'v', body = '%s' % string_table[value])
+ elif cell.data_type == cell.TYPE_FORMULA:
+ tag(doc, 'f', body = '%s' % value[1:])
+ tag(doc, 'v')
+ elif cell.data_type == cell.TYPE_NUMERIC:
+ tag(doc, 'v', body = '%s' % value)
+ else:
+ tag(doc, 'v', body = '%s' % value)
+ end_tag(doc, 'c')
+ end_tag(doc, 'row')
+ end_tag(doc, 'sheetData')
+
+
+def write_worksheet_hyperlinks(doc, worksheet):
+ """Write worksheet hyperlinks to xml."""
+ write_hyperlinks = False
+ for cell in worksheet.get_cell_collection():
+ if cell.hyperlink_rel_id is not None:
+ write_hyperlinks = True
+ break
+ if write_hyperlinks:
+ start_tag(doc, 'hyperlinks')
+ for cell in worksheet.get_cell_collection():
+ if cell.hyperlink_rel_id is not None:
+ attrs = {'display': cell.hyperlink,
+ 'ref': cell.get_coordinate(),
+ 'r:id': cell.hyperlink_rel_id}
+ tag(doc, 'hyperlink', attrs)
+ end_tag(doc, 'hyperlinks')
+
+
+def write_worksheet_rels(worksheet, idx):
+ """Write relationships for the worksheet to xml."""
+ root = Element('Relationships', {'xmlns': 'http://schemas.openxmlformats.org/package/2006/relationships'})
+ for rel in worksheet.relationships:
+ attrs = {'Id': rel.id, 'Type': rel.type, 'Target': rel.target}
+ if rel.target_mode:
+ attrs['TargetMode'] = rel.target_mode
+ SubElement(root, 'Relationship', attrs)
+ if worksheet._charts:
+ attrs = {'Id' : 'rId1',
+ 'Type' : 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
+ 'Target' : '../drawings/drawing%s.xml' % idx }
+ SubElement(root, 'Relationship', attrs)
+ return get_document_content(root)
diff --git a/test_tablib.py b/test_tablib.py
index 7791935..474323a 100755
--- a/test_tablib.py
+++ b/test_tablib.py
@@ -222,6 +222,8 @@ class TablibTestCase(unittest.TestCase):
data.csv
data.tsv
data.xls
+ data.xlsx
+ data.html
def test_book_export_no_exceptions(self):
@@ -233,6 +235,7 @@ class TablibTestCase(unittest.TestCase):
book.json
book.yaml
book.xls
+ book.xlsx
def test_json_import_set(self):
@@ -490,26 +493,26 @@ class TablibTestCase(unittest.TestCase):
def test_formatters(self):
"""Confirm formatters are being triggered."""
-
+
def _formatter(cell_value):
return str(cell_value).upper()
-
+
self.founders.add_formatter('last_name', _formatter)
-
+
for name in [r['last_name'] for r in self.founders.dict]:
self.assertTrue(name.isupper())
def test_unicode_csv(self):
"""Check if unicode in csv export doesn't raise."""
-
+
data = tablib.Dataset()
-
+
if sys.version_info[0] > 2:
data.append(['\xfc', '\xfd'])
else:
exec("data.append([u'\xfc', u'\xfd'])")
-
-
+
+
data.csv
if __name__ == '__main__':
diff --git a/test_tablib.py.orig b/test_tablib.py.orig
new file mode 100755
index 0000000..21131bb
--- /dev/null
+++ b/test_tablib.py.orig
@@ -0,0 +1,522 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""Tests for Tablib."""
+
+import unittest
+import sys
+
+if sys.version_info[0] > 2:
+ from tablib.packages import markup3 as markup
+else:
+ from tablib.packages import markup
+
+
+
+import tablib
+
+
+
+class TablibTestCase(unittest.TestCase):
+ """Tablib test cases."""
+
+ def setUp(self):
+ """Create simple data set with headers."""
+
+ global data, book
+
+ data = tablib.Dataset()
+ book = tablib.Databook()
+
+ self.headers = ('first_name', 'last_name', 'gpa')
+ self.john = ('John', 'Adams', 90)
+ self.george = ('George', 'Washington', 67)
+ self.tom = ('Thomas', 'Jefferson', 50)
+
+ self.founders = tablib.Dataset(headers=self.headers)
+ self.founders.append(self.john)
+ self.founders.append(self.george)
+ self.founders.append(self.tom)
+
+
+ def tearDown(self):
+ """Teardown."""
+ pass
+
+
+ def test_empty_append(self):
+ """Verify append() correctly adds tuple with no headers."""
+ new_row = (1, 2, 3)
+ data.append(new_row)
+
+ # Verify width/data
+ self.assertTrue(data.width == len(new_row))
+ self.assertTrue(data[0] == new_row)
+
+
+ def test_empty_append_with_headers(self):
+ """Verify append() correctly detects mismatch of number of
+ headers and data.
+ """
+ data.headers = ['first', 'second']
+ new_row = (1, 2, 3, 4)
+
+ self.assertRaises(tablib.InvalidDimensions, data.append, new_row)
+
+
+ def test_add_column(self):
+ """Verify adding column works with/without headers."""
+
+ data.append(['kenneth'])
+ data.append(['bessie'])
+
+ new_col = ['reitz', 'monke']
+
+ data.append(col=new_col)
+
+ self.assertEquals(data[0], ('kenneth', 'reitz'))
+ self.assertEquals(data.width, 2)
+
+ # With Headers
+ data.headers = ('fname', 'lname')
+ new_col = [21, 22]
+ data.append(col=new_col, header='age')
+
+ self.assertEquals(data['age'], new_col)
+
+
+ def test_add_column_no_data_no_headers(self):
+ """Verify adding new column with no headers."""
+
+ new_col = ('reitz', 'monke')
+
+ data.append(col=new_col)
+
+ self.assertEquals(data[0], tuple([new_col[0]]))
+ self.assertEquals(data.width, 1)
+ self.assertEquals(data.height, len(new_col))
+
+
+ def test_add_callable_column(self):
+ """Verify adding column with values specified as callable."""
+ new_col = [lambda x: x[0]]
+ self.founders.append(col=new_col, header='first_again')
+#
+# self.assertTrue(map(lambda x: x[0] == x[-1], self.founders))
+
+
+ def test_header_slicing(self):
+ """Verify slicing by headers."""
+
+ self.assertEqual(self.founders['first_name'],
+ [self.john[0], self.george[0], self.tom[0]])
+ self.assertEqual(self.founders['last_name'],
+ [self.john[1], self.george[1], self.tom[1]])
+ self.assertEqual(self.founders['gpa'],
+ [self.john[2], self.george[2], self.tom[2]])
+
+
+ def test_data_slicing(self):
+ """Verify slicing by data."""
+
+ # Slice individual rows
+ self.assertEqual(self.founders[0], self.john)
+ self.assertEqual(self.founders[:1], [self.john])
+ self.assertEqual(self.founders[1:2], [self.george])
+ self.assertEqual(self.founders[-1], self.tom)
+ self.assertEqual(self.founders[3:], [])
+
+ # Slice multiple rows
+ self.assertEqual(self.founders[:], [self.john, self.george, self.tom])
+ self.assertEqual(self.founders[0:2], [self.john, self.george])
+ self.assertEqual(self.founders[1:3], [self.george, self.tom])
+ self.assertEqual(self.founders[2:], [self.tom])
+
+
+ def test_delete(self):
+ """Verify deleting from dataset works."""
+
+ # Delete from front of object
+ del self.founders[0]
+ self.assertEqual(self.founders[:], [self.george, self.tom])
+
+ # Verify dimensions, width should NOT change
+ self.assertEqual(self.founders.height, 2)
+ self.assertEqual(self.founders.width, 3)
+
+ # Delete from back of object
+ del self.founders[1]
+ self.assertEqual(self.founders[:], [self.george])
+
+ # Verify dimensions, width should NOT change
+ self.assertEqual(self.founders.height, 1)
+ self.assertEqual(self.founders.width, 3)
+
+ # Delete from invalid index
+ self.assertRaises(IndexError, self.founders.__delitem__, 3)
+
+
+ def test_csv_export(self):
+ """Verify exporting dataset object as CSV."""
+
+ # Build up the csv string with headers first, followed by each row
+ csv = ''
+ for col in self.headers:
+ csv += col + ','
+
+ csv = csv.strip(',') + '\r\n'
+
+ for founder in self.founders:
+ for col in founder:
+ csv += str(col) + ','
+ csv = csv.strip(',') + '\r\n'
+
+ self.assertEqual(csv, self.founders.csv)
+
+ def test_tsv_export(self):
+ """Verify exporting dataset object as CSV."""
+
+ # Build up the csv string with headers first, followed by each row
+ tsv = ''
+ for col in self.headers:
+ tsv += col + '\t'
+
+ tsv = tsv.strip('\t') + '\r\n'
+
+ for founder in self.founders:
+ for col in founder:
+ tsv += str(col) + '\t'
+ tsv = tsv.strip('\t') + '\r\n'
+
+ self.assertEqual(tsv, self.founders.tsv)
+
+ def test_html_export(self):
+
+ """HTML export"""
+
+ html = markup.page()
+ html.table.open()
+ html.thead.open()
+
+ html.tr(markup.oneliner.th(self.founders.headers))
+ html.thead.close()
+
+ for founder in self.founders:
+
+ html.tr(markup.oneliner.td(founder))
+
+ html.table.close()
+ html = str(html)
+
+ self.assertEqual(html, self.founders.html)
+
+
+ def test_unicode_append(self):
+ """Passes in a single unicode charecter and exports."""
+
+ new_row = ('å', 'é')
+ data.append(new_row)
+
+ data.json
+ data.yaml
+ data.csv
+ data.tsv
+ data.xls
+<<<<<<< HEAD
+ data.html
+=======
+ data.xlsx
+>>>>>>> 5350355fbe0aefe053d40fda03c0688a7b7eae3d
+
+
+ def test_book_export_no_exceptions(self):
+ """Test that varoius exports don't error out."""
+
+ book = tablib.Databook()
+ book.add_sheet(data)
+
+ book.json
+ book.yaml
+ book.xls
+ book.xlsx
+
+
+ def test_json_import_set(self):
+ """Generate and import JSON set serialization."""
+ data.append(self.john)
+ data.append(self.george)
+ data.headers = self.headers
+
+ _json = data.json
+
+ data.json = _json
+
+ self.assertEqual(_json, data.json)
+
+
+ def test_json_import_book(self):
+ """Generate and import JSON book serialization."""
+ data.append(self.john)
+ data.append(self.george)
+ data.headers = self.headers
+
+ book.add_sheet(data)
+ _json = book.json
+
+ book.json = _json
+
+ self.assertEqual(_json, book.json)
+
+
+ def test_yaml_import_set(self):
+ """Generate and import YAML set serialization."""
+ data.append(self.john)
+ data.append(self.george)
+ data.headers = self.headers
+
+ _yaml = data.yaml
+
+ data.yaml = _yaml
+
+ self.assertEqual(_yaml, data.yaml)
+
+
+ def test_yaml_import_book(self):
+ """Generate and import YAML book serialization."""
+ data.append(self.john)
+ data.append(self.george)
+ data.headers = self.headers
+
+ book.add_sheet(data)
+ _yaml = book.yaml
+
+ book.yaml = _yaml
+
+ self.assertEqual(_yaml, book.yaml)
+
+
+ def test_csv_import_set(self):
+ """Generate and import CSV set serialization."""
+ data.append(self.john)
+ data.append(self.george)
+ data.headers = self.headers
+
+ _csv = data.csv
+
+ data.csv = _csv
+
+ self.assertEqual(_csv, data.csv)
+
+
+ def test_csv_import_set_with_spaces(self):
+ """Generate and import CSV set serialization when row values have
+ spaces."""
+ data.append(('Bill Gates', 'Microsoft'))
+ data.append(('Steve Jobs', 'Apple'))
+ data.headers = ('Name', 'Company')
+
+ _csv = data.csv
+
+ data.csv = _csv
+
+ self.assertEqual(_csv, data.csv)
+
+
+ def test_tsv_import_set(self):
+ """Generate and import TSV set serialization."""
+ data.append(self.john)
+ data.append(self.george)
+ data.headers = self.headers
+
+ _tsv = data.tsv
+
+ data.tsv = _tsv
+
+ self.assertEqual(_tsv, data.tsv)
+
+
+ def test_csv_format_detect(self):
+ """Test CSV format detection."""
+
+ _csv = (
+ '1,2,3\n'
+ '4,5,6\n'
+ '7,8,9\n'
+ )
+ _bunk = (
+ '¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
+ )
+
+ self.assertTrue(tablib.formats.csv.detect(_csv))
+ self.assertFalse(tablib.formats.csv.detect(_bunk))
+
+
+ def test_tsv_format_detect(self):
+ """Test TSV format detection."""
+
+ _tsv = (
+ '1\t2\t3\n'
+ '4\t5\t6\n'
+ '7\t8\t9\n'
+ )
+ _bunk = (
+ '¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
+ )
+
+ self.assertTrue(tablib.formats.tsv.detect(_tsv))
+ self.assertFalse(tablib.formats.tsv.detect(_bunk))
+
+
+ def test_json_format_detect(self):
+ """Test JSON format detection."""
+
+ _json = '[{"last_name": "Adams","age": 90,"first_name": "John"}]'
+ _bunk = (
+ '¡¡¡¡¡¡¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
+ )
+
+ self.assertTrue(tablib.formats.json.detect(_json))
+ self.assertFalse(tablib.formats.json.detect(_bunk))
+
+
+ def test_yaml_format_detect(self):
+ """Test YAML format detection."""
+
+ _yaml = '- {age: 90, first_name: John, last_name: Adams}'
+ _bunk = (
+ '¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
+ )
+
+ self.assertTrue(tablib.formats.yaml.detect(_yaml))
+ self.assertFalse(tablib.formats.yaml.detect(_bunk))
+
+
+ def test_auto_format_detect(self):
+ """Test auto format detection."""
+
+ _yaml = '- {age: 90, first_name: John, last_name: Adams}'
+ _json = '[{"last_name": "Adams","age": 90,"first_name": "John"}]'
+ _csv = '1,2,3\n4,5,6\n7,8,9\n'
+ _bunk = '¡¡¡¡¡¡---///\n\n\n¡¡£™∞¢£§∞§¶•¶ª∞¶•ªº••ª–º§•†•§º¶•†¥ª–º•§ƒø¥¨©πƒø†ˆ¥ç©¨√øˆ¥≈†ƒ¥ç©ø¨çˆ¥ƒçø¶'
+
+ self.assertEqual(tablib.detect(_yaml)[0], tablib.formats.yaml)
+ self.assertEqual(tablib.detect(_csv)[0], tablib.formats.csv)
+ self.assertEqual(tablib.detect(_json)[0], tablib.formats.json)
+ self.assertEqual(tablib.detect(_bunk)[0], None)
+
+
+ def test_transpose(self):
+ """Transpose a dataset."""
+
+ transposed_founders = self.founders.transpose()
+ first_row = transposed_founders[0]
+ second_row = transposed_founders[1]
+
+ self.assertEqual(transposed_founders.headers,
+ ["first_name","John", "George", "Thomas"])
+ self.assertEqual(first_row,
+ ("last_name","Adams", "Washington", "Jefferson"))
+ self.assertEqual(second_row,
+ ("gpa",90, 67, 50))
+
+
+ def test_row_stacking(self):
+
+ """Row stacking."""
+
+ to_join = tablib.Dataset(headers=self.founders.headers)
+
+ for row in self.founders:
+ to_join.append(row=row)
+
+ row_stacked = self.founders.stack_rows(to_join)
+
+ for column in row_stacked.headers:
+
+ original_data = self.founders[column]
+ expected_data = original_data + original_data
+ self.assertEqual(row_stacked[column], expected_data)
+
+
+ def test_column_stacking(self):
+
+ """Column stacking"""
+
+ to_join = tablib.Dataset(headers=self.founders.headers)
+
+ for row in self.founders:
+ to_join.append(row=row)
+
+ column_stacked = self.founders.stack_columns(to_join)
+
+ for index, row in enumerate(column_stacked):
+
+ original_data = self.founders[index]
+ expected_data = original_data + original_data
+ self.assertEqual(row, expected_data)
+
+ self.assertEqual(column_stacked[0],
+ ("John", "Adams", 90, "John", "Adams", 90))
+
+
+ def test_sorting(self):
+
+ """Sort columns."""
+
+ sorted_data = self.founders.sort(col="first_name")
+
+ first_row = sorted_data[0]
+ second_row = sorted_data[2]
+ third_row = sorted_data[1]
+ expected_first = self.founders[1]
+ expected_second = self.founders[2]
+ expected_third = self.founders[0]
+
+ self.assertEqual(first_row, expected_first)
+ self.assertEqual(second_row, expected_second)
+ self.assertEqual(third_row, expected_third)
+
+
+ def test_wipe(self):
+ """Purge a dataset."""
+
+ new_row = (1, 2, 3)
+ data.append(new_row)
+
+ # Verify width/data
+ self.assertTrue(data.width == len(new_row))
+ self.assertTrue(data[0] == new_row)
+
+ data.wipe()
+ new_row = (1, 2, 3, 4)
+ data.append(new_row)
+ self.assertTrue(data.width == len(new_row))
+ self.assertTrue(data[0] == new_row)
+
+
+ def test_formatters(self):
+ """Confirm formatters are being triggered."""
+
+ def _formatter(cell_value):
+ return str(cell_value).upper()
+
+ self.founders.add_formatter('last_name', _formatter)
+
+ for name in [r['last_name'] for r in self.founders.dict]:
+ self.assertTrue(name.isupper())
+
+ def test_unicode_csv(self):
+ """Check if unicode in csv export doesn't raise."""
+
+ data = tablib.Dataset()
+
+ if sys.version_info[0] > 2:
+ data.append(['\xfc', '\xfd'])
+ else:
+ exec("data.append([u'\xfc', u'\xfd'])")
+
+
+ data.csv
+
+if __name__ == '__main__':
+ unittest.main()