diff options
| author | Kenneth Reitz <me@kennethreitz.com> | 2011-05-13 01:15:06 -0400 |
|---|---|---|
| committer | Kenneth Reitz <me@kennethreitz.com> | 2011-05-13 01:15:06 -0400 |
| commit | 9146de36d45d2a6bc9adb0375bcc186d9b7167be (patch) | |
| tree | 06cb94b5709211c132c41699af70fbc328b670d1 | |
| parent | 865ce6278296cb0998e5544bdfa4e4e8983d7bb2 (diff) | |
| parent | 9761ff5e9ef3bed0cd4c7e9a00e5c10bd453e6c7 (diff) | |
| download | tablib-9146de36d45d2a6bc9adb0375bcc186d9b7167be.tar.gz | |
Merge branch 'release/0.9.7'v0.9.7
77 files changed, 12000 insertions, 860 deletions
@@ -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) © 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 @@ -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() |
