mirror of
https://github.com/kennethreitz/tablib.git
synced 2026-06-05 15:00:19 +00:00
Compare commits
375 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edbb16ec97 | |||
| dec5cea722 | |||
| 38183938dc | |||
| 7f1db4023f | |||
| b09fface1b | |||
| 69edb9def3 | |||
| ec54918f4a | |||
| ab6633549f | |||
| 56005d8022 | |||
| 36fa7ef097 | |||
| bb0abc863e | |||
| 58f6eefe01 | |||
| e4726cb85c | |||
| 412e690289 | |||
| 44e797d70e | |||
| 34c14aca18 | |||
| 7c318adde4 | |||
| 5dd74c0104 | |||
| a50ff92ff2 | |||
| 383d4b9c4e | |||
| 00e2ffa2ef | |||
| a3cd2c9cff | |||
| d89d243a30 | |||
| 69abfc3ada | |||
| 05bd0d1d42 | |||
| 62807734bd | |||
| c5c2dffe42 | |||
| 46102d4be7 | |||
| 44e9e24fec | |||
| 0ca5520bbc | |||
| e66eb4a189 | |||
| 0e720d78ca | |||
| 6afe716d64 | |||
| 76cbf9fadf | |||
| a93f93a458 | |||
| 3d44bdec40 | |||
| 319505817a | |||
| 6cb9a69746 | |||
| bb1354b61f | |||
| ddc4bd30f2 | |||
| 52e547daf9 | |||
| 7f0b7a0a22 | |||
| ddac443732 | |||
| e13f4d0aba | |||
| 54f9041f2c | |||
| 91d3299280 | |||
| f59abe84be | |||
| cf23f2344f | |||
| e16bb38c48 | |||
| 71ca275dd1 | |||
| 75bbfbbaf4 | |||
| b35d505621 | |||
| cd491c062c | |||
| 9fdb72cc5c | |||
| a5b1f7987e | |||
| 8cf6770a76 | |||
| 5fa3d2f886 | |||
| d4c66c7a4e | |||
| af17586581 | |||
| 23d21f00f3 | |||
| 7ee924b5a6 | |||
| d720beadac | |||
| ee9666a146 | |||
| 77a9e25795 | |||
| d515724817 | |||
| 2814fbc381 | |||
| 9ca1d4ec54 | |||
| abbb4e32d8 | |||
| f6e757d569 | |||
| 9ba0451843 | |||
| d99db57d75 | |||
| 2299c00883 | |||
| 5ba6f5d91a | |||
| bbdf5f11ab | |||
| 851ba25702 | |||
| 039272b274 | |||
| d6a7832e60 | |||
| e51c4faec7 | |||
| f7fc3244ee | |||
| 53d69bd3ea | |||
| fcc9700d11 | |||
| 1ec9c18a66 | |||
| 99c28fa560 | |||
| fa7fb579fd | |||
| be24de19dc | |||
| 1d4f4b68ca | |||
| 8debeb26ac | |||
| 38e1ee6c3d | |||
| a774789252 | |||
| 995eabad37 | |||
| d90358bf69 | |||
| c5920249de | |||
| 9b6a73c97c | |||
| 679bd115b6 | |||
| 32cbc36fc1 | |||
| 8bded88559 | |||
| f8f57a467e | |||
| a11a993955 | |||
| 25894f2948 | |||
| 591b89693e | |||
| 85d9c2497e | |||
| eaf52b691e | |||
| 6f53c5d2b9 | |||
| 90ee799576 | |||
| c02a21ccd2 | |||
| fa045ca114 | |||
| 65703550c3 | |||
| 1fcb98f9ae | |||
| e2d45ecff7 | |||
| 47d92277cc | |||
| fdd74b5b0c | |||
| de052f0fac | |||
| 2f3acf5af4 | |||
| c4e8755cd2 | |||
| 79dc4524a0 | |||
| a785d77901 | |||
| b3485ec942 | |||
| 28b358c9da | |||
| 24657520e9 | |||
| 66d9e50984 | |||
| 541fba6786 | |||
| bc6398ffb0 | |||
| dca7bc9a7d | |||
| 2fbda0f43d | |||
| e350f9428b | |||
| 68dba0a77d | |||
| 028be03c2c | |||
| e1d65ba3c8 | |||
| e4cb3bcd9b | |||
| bf9510e0c7 | |||
| 82ae3ca507 | |||
| 5fbdd56fba | |||
| f187cef5f4 | |||
| 87892d7266 | |||
| 20e2ce5ba0 | |||
| 48e576954d | |||
| a21f8187f8 | |||
| 8479df725e | |||
| 333deb2311 | |||
| 0b714f21e1 | |||
| ae730b00b1 | |||
| 84e8b0384f | |||
| 7a2842a8af | |||
| 954bbdccf3 | |||
| 7acaa8460d | |||
| 84e7e251ae | |||
| dc868eff31 | |||
| 43356e908c | |||
| f7acc19523 | |||
| c5972db8f0 | |||
| 1cc051f3e8 | |||
| 3da155ce0d | |||
| 9a34cf0980 | |||
| 434f66b4eb | |||
| d056916c53 | |||
| cf5239f097 | |||
| 49d8cb816f | |||
| fbd277ff2e | |||
| 6f4572fa56 | |||
| 453fc8614c | |||
| 01cf58e431 | |||
| f6cd89c76c | |||
| 1e0f30e8a6 | |||
| 569d35bfca | |||
| d40cdfbcd0 | |||
| 86bbaf9bea | |||
| 0ed01d85b9 | |||
| fc4cc7fa14 | |||
| 70716fdd21 | |||
| 1146ec2341 | |||
| 1a7d597745 | |||
| 56b627a561 | |||
| 98e182bed2 | |||
| c8a5563309 | |||
| c225a64d68 | |||
| d611d5a14f | |||
| 45121ddd65 | |||
| c74357cb20 | |||
| 939b0af551 | |||
| 9c2018653f | |||
| 2bc6122ee8 | |||
| 7f0748aac9 | |||
| 41a5c67159 | |||
| 3efefcc8da | |||
| d19de6025b | |||
| 65ba937c0d | |||
| 79a2bb888f | |||
| 25eacaf6f0 | |||
| c2a9af7fb3 | |||
| 3b06f3760d | |||
| e7ee3195a7 | |||
| 5bd2e3df52 | |||
| 837b3f83e6 | |||
| ff8f23edd5 | |||
| 5ffcfd56f2 | |||
| 955c24c974 | |||
| 192a5efabb | |||
| 1aafc7e2f4 | |||
| 9e45b95d12 | |||
| d8f0a018ae | |||
| 7545f3726e | |||
| 85e2bd73fc | |||
| 37033903c5 | |||
| 02c38c2520 | |||
| 26748deb9f | |||
| 63f6cea132 | |||
| 1b035f9774 | |||
| 2c14486c33 | |||
| 8bc69c9d85 | |||
| d36a2cbd42 | |||
| 1ab0eb3fae | |||
| cd71e1a5b1 | |||
| 47f79a7ca1 | |||
| 9f38efe413 | |||
| 5d98239a7e | |||
| a3f0d02633 | |||
| b29007a0df | |||
| e75c3c1a66 | |||
| 47cebbc328 | |||
| e4c39524f7 | |||
| c88c794314 | |||
| 752443f077 | |||
| 7c0507bcce | |||
| 652ac85549 | |||
| 05ea3c35fc | |||
| d5fada7e1d | |||
| 511c58d4e1 | |||
| c469360a0e | |||
| 97b4401b18 | |||
| 40e0f41b4c | |||
| 39435727ba | |||
| eda9d5af03 | |||
| 15435047c6 | |||
| a3781e3c89 | |||
| 6a825a8a39 | |||
| 6a449d497a | |||
| d807c60346 | |||
| 71603662b1 | |||
| 21c11b9911 | |||
| e8c923d712 | |||
| bc581c08df | |||
| 4f9c9d09ec | |||
| 63e8a7172d | |||
| 45e0af9f0e | |||
| fa6f5b3af3 | |||
| 0528e0a500 | |||
| 8e83734985 | |||
| 783eccc67d | |||
| 7236415f42 | |||
| c0a3c3ea1e | |||
| 14bd964fb1 | |||
| 6bfc6634ba | |||
| 54affad292 | |||
| 7c963a0f4d | |||
| 02f27f15c5 | |||
| 9c65515e7a | |||
| c87a954a9e | |||
| 42e40ed0ab | |||
| 23ab6c4724 | |||
| 32a09ccd6a | |||
| 81a7f79b3d | |||
| 05c9b33003 | |||
| ec7273d02d | |||
| 19ee1997b5 | |||
| f01d65c2e9 | |||
| 9778a96351 | |||
| 906138b138 | |||
| 43c68b396f | |||
| d611233c80 | |||
| 3d02b866ce | |||
| 887ee2fbac | |||
| bfd211854a | |||
| bc75911500 | |||
| a2b4e4c6ba | |||
| fde6f11763 | |||
| 33a83316df | |||
| f6d7888d9e | |||
| c19e2f2c5b | |||
| eaa2b9b8ea | |||
| 2f8083bda6 | |||
| 2c5a9af76e | |||
| e74a8f41cc | |||
| cd5aa4fc06 | |||
| 1d460bac40 | |||
| 4a3fde37a3 | |||
| 62ad123ad8 | |||
| fefc7b4d1f | |||
| 6313437a27 | |||
| 23a5bb1443 | |||
| 864f29cc4b | |||
| c136b794a7 | |||
| d254c2d2b0 | |||
| 9b235150cf | |||
| 9f3e6eeaa1 | |||
| 51728f954f | |||
| 2949b7c656 | |||
| 07d243bbc9 | |||
| bf3484e606 | |||
| 9b2ab6fae9 | |||
| 7a3d55daab | |||
| eec0595c5c | |||
| 0c7c248b96 | |||
| 0d14f7f2b9 | |||
| d5f713024d | |||
| 415bc819e7 | |||
| 974258094e | |||
| ab16f69be6 | |||
| 28d9af852a | |||
| 39c6ea6503 | |||
| 39b66ad8e9 | |||
| 004b3da680 | |||
| d4923533eb | |||
| 29e0b76910 | |||
| 4f54de2630 | |||
| 1f0d68ee79 | |||
| cae8fa1276 | |||
| 4c0a20a7b9 | |||
| 6c1fa87138 | |||
| 0e30255836 | |||
| 1156d5a220 | |||
| 83b71967b9 | |||
| 4dab48cd76 | |||
| 5324526329 | |||
| 1dfcd42233 | |||
| f162b19bd6 | |||
| 707164e459 | |||
| 42f0a285c3 | |||
| d111cc7cc7 | |||
| 25fe211a22 | |||
| 4b675494c4 | |||
| a196b9a5dd | |||
| 5ba56c2bb3 | |||
| 36fbdda492 | |||
| 273d2729ee | |||
| 3036bc9e52 | |||
| b9c74eacc8 | |||
| 805ccfae34 | |||
| fddc018394 | |||
| 2477100062 | |||
| 983b979fda | |||
| 3edb45bac7 | |||
| 29d626fa1f | |||
| 1f22fc7321 | |||
| 8631f60f8d | |||
| 65873b6112 | |||
| 56e44bd45c | |||
| 87e65fd3e7 | |||
| ffbc3b122d | |||
| 9d71603dad | |||
| cceb41af98 | |||
| 60ffa898fd | |||
| a4a211b5a6 | |||
| c9766a48b0 | |||
| 6975685b89 | |||
| e920244a1b | |||
| ea63779baf | |||
| d826f6d0ae | |||
| f6fa3f2abc | |||
| eed6df45e0 | |||
| cb4c67767a | |||
| 1e21fee70e | |||
| 420dd36ab8 | |||
| 9a05770899 | |||
| 8e055f1c57 | |||
| 239e33aaed | |||
| bf4fdea187 | |||
| 03086052ed | |||
| 2128473938 | |||
| 74c64d66a9 | |||
| a4e77f22c4 | |||
| 2e03046a07 | |||
| 06a7b4cd4e | |||
| 6a70b84166 | |||
| 77d9fe8b41 | |||
| 64cb547e0a |
+7
-1
@@ -22,4 +22,10 @@ coverage.xml
|
||||
nosetests.xml
|
||||
junit-py25.xml
|
||||
junit-py26.xml
|
||||
junit-py27.xml
|
||||
junit-py27.xml
|
||||
|
||||
# tox noise
|
||||
.tox
|
||||
|
||||
# pyenv noise
|
||||
.python-version
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
language: python
|
||||
python:
|
||||
- 2.7
|
||||
- 3.3
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
script: python test_tablib.py
|
||||
@@ -4,9 +4,14 @@ various contributors:
|
||||
Development Lead
|
||||
````````````````
|
||||
|
||||
- Kenneth Reitz <_@kennethreitz.com>
|
||||
- Kenneth Reitz <me@kennethreitz.org>
|
||||
|
||||
|
||||
Core Contributors
|
||||
`````````````````
|
||||
|
||||
- Iuri de Silvio <iurisilvio@gmail.com>
|
||||
|
||||
Patches and Suggestions
|
||||
```````````````````````
|
||||
|
||||
@@ -15,4 +20,17 @@ Patches and Suggestions
|
||||
- Luca Beltrame
|
||||
- Benjamin Wohlwend
|
||||
- Erik Youngren
|
||||
- Mark Rogers
|
||||
- Mark Rogers
|
||||
- Mark Walling
|
||||
- Mike Waldner
|
||||
- Joel Friedly
|
||||
- Jakub Janoszek
|
||||
- Marc Abramowitz
|
||||
- Alex Gaynor
|
||||
- James Douglass
|
||||
- Tommy Anthony
|
||||
- Rabin Nankhwa
|
||||
- Marco Dallagiacoma
|
||||
- Mathias Loesch
|
||||
- Tushar Makkar
|
||||
- Andrii Soldatenko
|
||||
|
||||
+97
@@ -1,6 +1,103 @@
|
||||
History
|
||||
-------
|
||||
|
||||
0.11.5 (2017-06-13)
|
||||
+++++++++++++++++++
|
||||
|
||||
- Use ``yaml.safe_load`` for importing yaml.
|
||||
|
||||
0.11.4 (2017-01-23)
|
||||
+++++++++++++++++++
|
||||
|
||||
- Use built-in `json` package if available
|
||||
- Support Python 3.5+ in classifiers
|
||||
|
||||
** Bugfixes **
|
||||
|
||||
- Fixed textual representation for Dataset with no headers
|
||||
- Handle decimal types
|
||||
|
||||
0.11.3 (2016-02-16)
|
||||
+++++++++++++++++++
|
||||
|
||||
- Release fix.
|
||||
|
||||
0.11.2 (2016-02-16)
|
||||
+++++++++++++++++++
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix export only formats.
|
||||
- Fix for xlsx output.
|
||||
|
||||
0.11.1 (2016-02-07)
|
||||
+++++++++++++++++++
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fixed packaging error on Python 3.
|
||||
|
||||
|
||||
0.11.0 (2016-02-07)
|
||||
+++++++++++++++++++
|
||||
|
||||
**New Formats!**
|
||||
|
||||
- Added LaTeX table export format (``Dataset.latex``).
|
||||
- Support for dBase (DBF) files (``Dataset.dbf``).
|
||||
|
||||
**Improvements**
|
||||
|
||||
- New import/export interface (``Dataset.export()``, ``Dataset.load()``).
|
||||
- CSV custom delimiter support (``Dataset.export('csv', delimiter='$')``).
|
||||
- Adding ability to remove duplicates to all rows in a dataset (``Dataset.remove_duplicates()``).
|
||||
- Added a mechanism to avoid ``datetime.datetime`` issues when serializing data.
|
||||
- New ``detect_format()`` function (mostly for internal use).
|
||||
- Update the vendored unicodecsv to fix ``None`` handling.
|
||||
- Only freeze the headers row, not the headers columns (xls).
|
||||
|
||||
**Breaking Changes**
|
||||
|
||||
- ``detect()`` function removed.
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix XLSX import.
|
||||
- Bugfix for ``Dataset.transpose().transpose()``.
|
||||
|
||||
|
||||
0.10.0 (2014-05-27)
|
||||
+++++++++++++++++++
|
||||
|
||||
* Unicode Column Headers
|
||||
* ALL the bugfixes!
|
||||
|
||||
0.9.11 (2011-06-30)
|
||||
+++++++++++++++++++
|
||||
|
||||
* Bugfixes
|
||||
|
||||
0.9.10 (2011-06-22)
|
||||
+++++++++++++++++++
|
||||
|
||||
* Bugfixes
|
||||
|
||||
0.9.9 (2011-06-21)
|
||||
++++++++++++++++++
|
||||
|
||||
* Dataset API Changes
|
||||
* ``stack_rows`` => ``stack``, ``stack_columns`` => ``stack_cols``
|
||||
* column operations have their own methods now (``append_col``, ``insert_col``)
|
||||
* List-style ``pop()``
|
||||
* Redis-style ``rpush``, ``lpush``, ``rpop``, ``lpop``, ``rpush_col``, and ``lpush_col``
|
||||
|
||||
0.9.8 (2011-05-22)
|
||||
++++++++++++++++++
|
||||
|
||||
* OpenDocument Spreadsheet support (.ods)
|
||||
* Full Unicode TSV support
|
||||
|
||||
|
||||
0.9.7 (2011-05-12)
|
||||
++++++++++++++++++
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2011 Kenneth Reitz.
|
||||
Copyright 2016 Kenneth Reitz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
include HISTORY.rst README.rst LICENSE AUTHORS
|
||||
include HISTORY.rst README.rst LICENSE AUTHORS NOTICE test_tablib.py
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
test:
|
||||
python test_tablib.py
|
||||
publish:
|
||||
python setup.py register
|
||||
python setup.py sdist upload
|
||||
python setup.py bdist_wheel --universal upload
|
||||
@@ -1,5 +1,4 @@
|
||||
Tablib includes some vendorized python libraries: ordereddict, pyyaml,
|
||||
simplejson, unicodecsv, and xlwt.
|
||||
Tablib includes some vendorized python libraries: ordereddict, markup.
|
||||
|
||||
Markup License
|
||||
==============
|
||||
@@ -7,7 +6,6 @@ Markup License
|
||||
Markup is in the public domain.
|
||||
|
||||
|
||||
|
||||
OrderedDict License
|
||||
===================
|
||||
|
||||
@@ -32,178 +30,3 @@ subject to the following conditions:
|
||||
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.
|
||||
|
||||
|
||||
|
||||
PyYAML License
|
||||
==============
|
||||
|
||||
Copyright (c) 2006 Kirill Simonov
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
AnyJSON License
|
||||
==================
|
||||
|
||||
This software is licensed under the ``New BSD License``:
|
||||
|
||||
Copyright (c) 2009, by the authors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
Neither the name of the authors nor the names of its contributors may be used
|
||||
to endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
|
||||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
UnicodeCSV License
|
||||
==================
|
||||
|
||||
Copyright 2010 Jeremy Dunck. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are
|
||||
permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
of conditions and the following disclaimer in the documentation and/or other materials
|
||||
provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY JEREMY DUNCK ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
||||
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JEREMY DUNCK OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The views and conclusions contained in the software and documentation are those of the
|
||||
authors and should not be interpreted as representing official policies, either expressed
|
||||
or implied, of Jeremy Dunck.
|
||||
|
||||
|
||||
|
||||
|
||||
XLWT License
|
||||
============
|
||||
|
||||
Portions copyright © 2007, Stephen John Machin, Lingfo Pty Ltd
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. None of the names of Stephen John Machin, Lingfo Pty Ltd and any
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
|
||||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright (C) 2005 Roman V. Kiseliov
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
3. All advertising materials mentioning features or use of this
|
||||
software must display the following acknowledgment:
|
||||
"This product includes software developed by
|
||||
Roman V. Kiseliov <roman@kiseliov.ru>."
|
||||
|
||||
4. Redistributions of any form whatsoever must retain the following
|
||||
acknowledgment:
|
||||
"This product includes software developed by
|
||||
Roman V. Kiseliov <roman@kiseliov.ru>."
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY Roman V. Kiseliov ``AS IS'' AND ANY
|
||||
EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Roman V. Kiseliov OR
|
||||
ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
||||
OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Roman V. Kiseliov
|
||||
Russia
|
||||
Kursk
|
||||
Libknecht St., 4
|
||||
|
||||
+7(0712)56-09-83
|
||||
|
||||
<roman@kiseliov.ru>
|
||||
Subject: pyExcelerator
|
||||
+33
-19
@@ -1,6 +1,9 @@
|
||||
Tablib: format-agnostic tabular dataset library
|
||||
===============================================
|
||||
|
||||
.. image:: https://travis-ci.org/kennethreitz/tablib.svg?branch=master
|
||||
:target: https://travis-ci.org/kennethreitz/tablib
|
||||
|
||||
::
|
||||
|
||||
_____ ______ ___________ ______
|
||||
@@ -18,9 +21,12 @@ Output formats supported:
|
||||
- Excel (Sets + Books)
|
||||
- JSON (Sets + Books)
|
||||
- YAML (Sets + Books)
|
||||
- Pandas DataFrames (Sets)
|
||||
- HTML (Sets)
|
||||
- TSV (Sets)
|
||||
- OSD (Sets)
|
||||
- CSV (Sets)
|
||||
- DBF (Sets)
|
||||
|
||||
Note that tablib *purposefully* excludes XML support. It always will. (Note: This is a joke. Pull requests are welcome.)
|
||||
|
||||
@@ -28,10 +34,10 @@ Overview
|
||||
--------
|
||||
|
||||
`tablib.Dataset()`
|
||||
A Dataset is a table of tabular data. It may or may not have a header row. They can be build and manipulated as raw Python datatypes (Lists of tuples|dictionaries). Datasets can be imported from JSON, YAML, and CSV; they can be exported to Excel (XLS), JSON, YAML, and CSV.
|
||||
A Dataset is a table of tabular data. It may or may not have a header row. They can be build and manipulated as raw Python datatypes (Lists of tuples|dictionaries). Datasets can be imported from JSON, YAML, DBF, and CSV; they can be exported to XLSX, XLS, ODS, JSON, YAML, DBF, CSV, TSV, and HTML.
|
||||
|
||||
`tablib.Databook()`
|
||||
A Databook is a set of Datasets. The most common form of a Databook is an Excel file with multiple spreadsheets. Databooks can be imported from JSON and YAML; they can be exported to Excel (XLS), JSON, and YAML.
|
||||
A Databook is a set of Datasets. The most common form of a Databook is an Excel file with multiple spreadsheets. Databooks can be imported from JSON and YAML; they can be exported to XLSX, XLS, ODS, JSON, and YAML.
|
||||
|
||||
Usage
|
||||
-----
|
||||
@@ -55,17 +61,17 @@ Intelligently add new rows: ::
|
||||
|
||||
Intelligently add new columns: ::
|
||||
|
||||
>>> data.append(col=(90, 67, 83), header='age')
|
||||
>>> data.append_col((90, 67, 83), header='age')
|
||||
|
||||
Slice rows: ::
|
||||
|
||||
>>> print data[:2]
|
||||
>>> print(data[:2])
|
||||
[('John', 'Adams', 90), ('George', 'Washington', 67)]
|
||||
|
||||
|
||||
Slice columns by header: ::
|
||||
|
||||
>>> print data['first_name']
|
||||
>>> print(data['first_name'])
|
||||
['John', 'George', 'Henry']
|
||||
|
||||
Easily delete rows: ::
|
||||
@@ -81,7 +87,7 @@ JSON!
|
||||
+++++
|
||||
::
|
||||
|
||||
>>> print data.json
|
||||
>>> print(data.export('json'))
|
||||
[
|
||||
{
|
||||
"last_name": "Adams",
|
||||
@@ -100,7 +106,7 @@ YAML!
|
||||
+++++
|
||||
::
|
||||
|
||||
>>> print data.yaml
|
||||
>>> print(data.export('yaml'))
|
||||
- {age: 90, first_name: John, last_name: Adams}
|
||||
- {age: 83, first_name: Henry, last_name: Ford}
|
||||
|
||||
@@ -108,7 +114,7 @@ CSV...
|
||||
++++++
|
||||
::
|
||||
|
||||
>>> print data.csv
|
||||
>>> print(data.export('csv'))
|
||||
first_name,last_name,age
|
||||
John,Adams,90
|
||||
Henry,Ford,83
|
||||
@@ -117,7 +123,24 @@ EXCEL!
|
||||
++++++
|
||||
::
|
||||
|
||||
>>> open('people.xls', 'wb').write(data.xls)
|
||||
>>> with open('people.xls', 'wb') as f:
|
||||
... f.write(data.export('xls'))
|
||||
|
||||
DBF!
|
||||
++++
|
||||
::
|
||||
|
||||
>>> with open('people.dbf', 'wb') as f:
|
||||
... f.write(data.export('dbf'))
|
||||
|
||||
Pandas DataFrame!
|
||||
+++++++++++++++++
|
||||
::
|
||||
|
||||
>>> print(data.export('df')):
|
||||
first_name last_name age
|
||||
0 John Adams 90
|
||||
1 Henry Ford 83
|
||||
|
||||
It's that easy.
|
||||
|
||||
@@ -129,9 +152,8 @@ To install tablib, simply: ::
|
||||
|
||||
$ pip install tablib
|
||||
|
||||
Or, if you absolutely must: ::
|
||||
Make sure to check out `Tablib on PyPi <https://pypi.python.org/pypi/tablib/>`_!
|
||||
|
||||
$ easy_install tablib
|
||||
|
||||
Contribute
|
||||
----------
|
||||
@@ -141,14 +163,6 @@ changes to the **develop** branch (or branch off of it), and send a pull
|
||||
request. Make sure you add yourself to AUTHORS_.
|
||||
|
||||
|
||||
Roadmap
|
||||
-------
|
||||
|
||||
v1.0.0:
|
||||
- Add hooks system
|
||||
- Tablib.ext namespace
|
||||
- Better 2.x/3.x handling (currently internal codebase fork)
|
||||
- Width detection on XLS out
|
||||
|
||||
|
||||
.. _`the repository`: http://github.com/kennethreitz/tablib
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
* Hooks System
|
||||
- pre/post-append
|
||||
- pre/post-import
|
||||
- pre/post-export
|
||||
* Add Tablib.ext namespace
|
||||
* Fix 2.x/3.x handling (currently internal codebase fork)
|
||||
* Make CSV write more customizable.
|
||||
* Width detection for XLS output
|
||||
* Documentation Improvements
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
<h3><a href="http://docs.python-tablib.org">About Tablib</a></h3>
|
||||
<p>
|
||||
Tablib is an MIT Licensed format-agnostic tabular dataset library, written in Python. It allows you to import, export, and manipulate tabular data sets. Advanced features include, segregation, dynamic columns, tags & filtering, and seamless format import & export.
|
||||
</p>
|
||||
|
||||
<h3>Feedback</h3>
|
||||
<p>
|
||||
Feedback is greatly appreciated. If you have any questions, comments,
|
||||
random praise, or anonymous threats, <a href="mailto:me@kennethreitz.com">
|
||||
shoot me an email</a>.
|
||||
</p>
|
||||
|
||||
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="http://docs.python-tablib.org/">The Tablib Website</a></li>
|
||||
<li><a href="http://pypi.python.org/pypi/tablib">Tablib @ PyPI</a></li>
|
||||
<li><a href="http://github.com/kennethreitz/tablib">Tablib @ GitHub</a></li>
|
||||
<li><a href="http://github.com/kennethreitz/tablib/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
<h3><a href="http://docs.python-tablib.org/">About Tablib</a></h3>
|
||||
<p>
|
||||
Tablib is an MIT Licensed format-agnostic tabular dataset library, written in Python. It allows you to import, export, and manipulate tabular data sets. Advanced features include, segregation, dynamic columns, tags & filtering, and seamless format import & export.
|
||||
</p>
|
||||
Vendored
+25
-2
@@ -9,10 +9,18 @@
|
||||
{% endblock %}
|
||||
{%- block relbar2 %}{% endblock %}
|
||||
{%- block footer %}
|
||||
<div class="footer">
|
||||
<div class="footer">
|
||||
© Copyright {{ copyright }}.
|
||||
Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
|
||||
</div>
|
||||
<a href="https://github.com/kennethreitz/tablib">
|
||||
<img style="position: absolute; top: 0; right: 0; border: 0;" src="//s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" />
|
||||
</a>
|
||||
|
||||
<script type="text/javascript" src="//www.hellobar.com/hellobar.js"></script>
|
||||
<script type="text/javascript">
|
||||
new HelloBar(36402,48802);
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
@@ -28,4 +36,19 @@
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var t = document.createElement('script');
|
||||
t.type = 'text/javascript';
|
||||
t.async = true;
|
||||
t.id = 'gauges-tracker';
|
||||
t.setAttribute('data-site-id',
|
||||
'4ddc284f613f5d2f1a000001');
|
||||
t.src = '//secure.gaug.es/track.js';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(t, s);
|
||||
})();
|
||||
</script>
|
||||
|
||||
{%- endblock %}
|
||||
|
||||
Vendored
+68
-28
@@ -8,11 +8,11 @@
|
||||
|
||||
{% set page_width = '940px' %}
|
||||
{% set sidebar_width = '220px' %}
|
||||
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
|
||||
body {
|
||||
font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro';
|
||||
font-size: 17px;
|
||||
@@ -43,7 +43,7 @@ div.sphinxsidebar {
|
||||
hr {
|
||||
border: 1px solid #B1B4B6;
|
||||
}
|
||||
|
||||
|
||||
div.body {
|
||||
background-color: #ffffff;
|
||||
color: #3E4349;
|
||||
@@ -54,7 +54,7 @@ img.floatingflask {
|
||||
padding: 0 0 10px 10px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
div.footer {
|
||||
width: {{ page_width }};
|
||||
margin: 20px auto 30px auto;
|
||||
@@ -70,7 +70,7 @@ div.footer a {
|
||||
div.related {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #444;
|
||||
text-decoration: none;
|
||||
@@ -80,7 +80,7 @@ div.sphinxsidebar a {
|
||||
div.sphinxsidebar a:hover {
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
@@ -95,7 +95,7 @@ div.sphinxsidebarwrapper p.logo {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar h3,
|
||||
div.sphinxsidebar h4 {
|
||||
font-family: 'Garamond', 'Georgia', serif;
|
||||
@@ -109,7 +109,7 @@ div.sphinxsidebar h4 {
|
||||
div.sphinxsidebar h4 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #444;
|
||||
}
|
||||
@@ -120,7 +120,7 @@ div.sphinxsidebar p.logo a:hover,
|
||||
div.sphinxsidebar h3 a:hover {
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: #555;
|
||||
margin: 10px 0;
|
||||
@@ -131,25 +131,25 @@ div.sphinxsidebar ul {
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #ccc;
|
||||
font-family: 'Georgia', serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
|
||||
a {
|
||||
color: #004B6B;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
a:hover {
|
||||
color: #6D4100;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
@@ -161,25 +161,25 @@ div.body h6 {
|
||||
margin: 30px 0px 10px 0px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
|
||||
div.body h2 { font-size: 180%; }
|
||||
div.body h3 { font-size: 150%; }
|
||||
div.body h4 { font-size: 130%; }
|
||||
div.body h5 { font-size: 100%; }
|
||||
div.body h6 { font-size: 100%; }
|
||||
|
||||
|
||||
a.headerlink {
|
||||
color: #ddd;
|
||||
padding: 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
a.headerlink:hover {
|
||||
color: #444;
|
||||
background: #eaeaea;
|
||||
}
|
||||
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
@@ -226,20 +226,20 @@ div.note {
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
|
||||
div.seealso {
|
||||
background-color: #ffc;
|
||||
border: 1px solid #ff6;
|
||||
}
|
||||
|
||||
|
||||
div.topic {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
|
||||
p.admonition-title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
@@ -333,7 +333,7 @@ ul, ol {
|
||||
margin: 10px 0 10px 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
pre {
|
||||
background: #eee;
|
||||
padding: 7px 30px;
|
||||
@@ -350,7 +350,7 @@ dl dl pre {
|
||||
margin-left: -90px;
|
||||
padding-left: 90px;
|
||||
}
|
||||
|
||||
|
||||
tt {
|
||||
background-color: #ecf0f3;
|
||||
color: #222;
|
||||
@@ -388,7 +388,7 @@ a:hover tt {
|
||||
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
|
||||
|
||||
div.sphinxsidebar {
|
||||
display: none;
|
||||
}
|
||||
@@ -423,8 +423,48 @@ a:hover tt {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* scrollbars */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:start:decrement,
|
||||
::-webkit-scrollbar-button:end:increment {
|
||||
display: block;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button:vertical:increment {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background-color: #eee;
|
||||
-webkit-border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:vertical {
|
||||
height: 50px;
|
||||
background-color: #ccc;
|
||||
-webkit-border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:horizontal {
|
||||
width: 50px;
|
||||
background-color: #ccc;
|
||||
-webkit-border-radius: 3px;
|
||||
}
|
||||
|
||||
/* misc. */
|
||||
|
||||
.revsys-inline {
|
||||
display: none!important;
|
||||
}
|
||||
Vendored
+2
-2
@@ -14,8 +14,8 @@
|
||||
{% block relbar1 %}{% endblock %}
|
||||
{% block relbar2 %}
|
||||
{% if theme_github_fork %}
|
||||
<a href="http://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;"
|
||||
src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a>
|
||||
<a href="https://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;"
|
||||
src="//s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block sidebar1 %}{% endblock %}
|
||||
|
||||
+7
-3
@@ -41,14 +41,14 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Tablib'
|
||||
copyright = u'2011, Kenneth Reitz. Styles (modified) © Armin Ronacher'
|
||||
copyright = u'2016. A <a href="http://kennethreitz.org/">Kenneth Reitz</a> Project'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.9.7'
|
||||
version = tablib.__version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
@@ -131,7 +131,11 @@ html_static_path = ['static']
|
||||
html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
html_sidebars = {
|
||||
'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'],
|
||||
'**': ['sidebarlogo.html', 'localtoc.html', 'relations.html',
|
||||
'sourcelink.html', 'searchbox.html']
|
||||
}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
|
||||
+29
-68
@@ -5,12 +5,8 @@ Development
|
||||
|
||||
Tablib is under active development, and contributors are welcome.
|
||||
|
||||
If you have a feature request, suggestion, or bug report, please open a new issue on GitHub_. To submit patches, please send a pull request on GitHub_.
|
||||
|
||||
If you'd like to contribute, there's plenty to do. Here's a short todo list.
|
||||
|
||||
.. include:: ../TODO.rst
|
||||
|
||||
If you have a feature request, suggestion, or bug report, please open a new
|
||||
issue on GitHub_. To submit patches, please send a pull request on GitHub_.
|
||||
|
||||
.. _GitHub: http://github.com/kennethreitz/tablib/
|
||||
|
||||
@@ -42,19 +38,18 @@ Source Control
|
||||
--------------
|
||||
|
||||
|
||||
Tablib source is controlled with Git_, the lean, mean, distributed source control machine.
|
||||
Tablib source is controlled with Git_, the lean, mean, distributed source
|
||||
control machine.
|
||||
|
||||
The repository is publicly accessable.
|
||||
The repository is publicly accessible.
|
||||
|
||||
``git clone git://github.com/kennethreitz/tablib.git``
|
||||
|
||||
The project is hosted both on **GitHub** and **git.kennethreitz.com**.
|
||||
|
||||
|
||||
GitHub:
|
||||
|
||||
The project is hosted on **GitHub**.
|
||||
|
||||
|
||||
GitHub:
|
||||
http://github.com/kennethreitz/tablib
|
||||
"Mirror":
|
||||
http://git.kennethreitz.com/projects/tablib
|
||||
|
||||
|
||||
Git Branch Structure
|
||||
@@ -66,12 +61,10 @@ Feature / Hotfix / Release branches follow a `Successful Git Branching Model`_ .
|
||||
The "next release" branch. Likely unstable.
|
||||
``master``
|
||||
Current production release (|version|) on PyPi.
|
||||
``gh-pages``
|
||||
Current release of http://tablib.org.
|
||||
|
||||
Each release is tagged.
|
||||
|
||||
When submitting patches, please place your feature/change in its own branch prior to opening a pull reqeust on GitHub_.
|
||||
When submitting patches, please place your feature/change in its own branch prior to opening a pull request on GitHub_.
|
||||
|
||||
|
||||
.. _Git: http://git-scm.org
|
||||
@@ -87,9 +80,7 @@ Adding New Formats
|
||||
|
||||
Tablib welcomes new format additions! Format suggestions include:
|
||||
|
||||
* Tab Separated Values
|
||||
* MySQL Dump
|
||||
* HTML Table
|
||||
|
||||
|
||||
Coding by Convention
|
||||
@@ -100,27 +91,27 @@ Tablib features a micro-framework for adding format support. The easiest way to
|
||||
1. Write a new format interface.
|
||||
|
||||
:class:`tablib.core` follows a simple pattern for automatically utilizing your format throughout Tablib. Function names are crucial.
|
||||
|
||||
|
||||
Example **tablib/formats/_xxx.py**: ::
|
||||
|
||||
title = 'xxx'
|
||||
|
||||
|
||||
def export_set(dset):
|
||||
....
|
||||
# returns string representation of given dataset
|
||||
|
||||
|
||||
def export_book(dbook):
|
||||
....
|
||||
# returns string representation of given databook
|
||||
|
||||
|
||||
def import_set(dset, in_stream):
|
||||
...
|
||||
# populates given Dataset with given datastream
|
||||
|
||||
|
||||
def import_book(dbook, in_stream):
|
||||
...
|
||||
# returns Databook instance
|
||||
|
||||
|
||||
def detect(stream):
|
||||
...
|
||||
# returns True if given stream is parsable as xxx
|
||||
@@ -130,9 +121,9 @@ Tablib features a micro-framework for adding format support. The easiest way to
|
||||
|
||||
If the format excludes support for an import/export mechanism (*eg.* :class:`csv <tablib.Dataset.csv>` excludes :class:`Databook <tablib.Databook>` support), simply don't define the respective functions. Appropriate errors will be raised.
|
||||
|
||||
2.
|
||||
2.
|
||||
|
||||
Add your new format module to the :class:`tablib.formats.avalable` tuple.
|
||||
Add your new format module to the :class:`tablib.formats.available` tuple.
|
||||
|
||||
3.
|
||||
Add a mock property to the :class:`Dataset <tablib.Dataset>` class with verbose `reStructured Text`_ docstring. This alleviates IDE confusion, and allows for pretty auto-generated Sphinx_ documentation.
|
||||
@@ -152,7 +143,7 @@ When developing a feature for Tablib, the easiest way to test your changes for p
|
||||
$ ./test_tablib.py
|
||||
|
||||
|
||||
`Hudson CI`_, amongst other tools, supports Java's xUnit testing report format. Nose_ allows us to generate our own xUnit reports.
|
||||
`Jenkins CI`_, amongst other tools, supports Java's xUnit testing report format. Nose_ allows us to generate our own xUnit reports.
|
||||
|
||||
Installing nose is simple. ::
|
||||
|
||||
@@ -168,26 +159,21 @@ This will generate a **nosetests.xml** file, which can then be analyzed.
|
||||
|
||||
|
||||
|
||||
.. _hudson:
|
||||
.. _jenkins:
|
||||
|
||||
----------------------
|
||||
Continuous Integration
|
||||
----------------------
|
||||
|
||||
Every commit made to the **develop** branch is automatically tested and inspected upon receipt with `Hudson CI`_. If you have access to the main repository and broke the build, you will receive an email accordingly.
|
||||
Every commit made to the **develop** branch is automatically tested and inspected upon receipt with `Travis CI`_. If you have access to the main repository and broke the build, you will receive an email accordingly.
|
||||
|
||||
Anyone may view the build status and history at any time.
|
||||
|
||||
http://ci.kennethreitz.com/
|
||||
|
||||
|
||||
If you are trustworthy and plan to contribute to tablib on a regular basis, please contact `Kenneth Reitz`_ to get an account on the Hudson Server.
|
||||
|
||||
https://travis-ci.org/kennethreitz/tablib
|
||||
|
||||
Additional reports will also be included here in the future, including :pep:`8` checks and stress reports for extremely large datasets.
|
||||
|
||||
.. _`Hudson CI`: http://hudson.dev.java.net
|
||||
.. _`Kenneth Reitz`: http://kennethreitz.com/contact-me/
|
||||
.. _`Jenkins CI`: https://travis-ci.org/
|
||||
|
||||
|
||||
.. _docs:
|
||||
@@ -196,51 +182,26 @@ Additional reports will also be included here in the future, including :pep:`8`
|
||||
Building the Docs
|
||||
-----------------
|
||||
|
||||
Documentation is written in the powerful, flexible, and standard Python documentation format, `reStructured Text`_.
|
||||
Documentation is written in the powerful, flexible, and standard Python documentation format, `reStructured Text`_.
|
||||
Documentation builds are powered by the powerful Pocoo project, Sphinx_. The :ref:`API Documentation <api>` is mostly documented inline throughout the module.
|
||||
|
||||
The Docs live in ``tablib/docs``. In order to build them, you will first need to install Sphinx. ::
|
||||
|
||||
$ pip install sphinx
|
||||
|
||||
|
||||
|
||||
Then, to build an HTML version of the docs, simply run the following from the **docs** directory: ::
|
||||
|
||||
$ make html
|
||||
$ make html
|
||||
|
||||
Your ``docs/_build/html`` directory will then contain an HTML representation of the documentation, ready for publication on most web servers.
|
||||
|
||||
You can also generate the documentation in **ebpub**, **latex**, **json**, *&c* similarly.
|
||||
|
||||
.. admonition:: GitHub Pages
|
||||
|
||||
To push the documentation up to `GitHub Pages`_, you will first need to run `sphinx-to-github`_ against your ``docs/_build/html`` directory.
|
||||
|
||||
GitHub Pages are powered by an HTML generation system called Jeckyl_, which is configured to ignore files and folders that begin with "``_``" (*ie.* **_static**).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
and `sphinx-to-github`_. ::
|
||||
|
||||
Installing sphinx-to-github is simple. ::
|
||||
|
||||
$ pip install sphinx-to-github
|
||||
|
||||
Running it against the docs is even simpler. ::
|
||||
|
||||
$ sphinx-to-github _build/html
|
||||
|
||||
Move the resulting files to the **gh-pages** branch of your repository, and push it up to GitHub.
|
||||
You can also generate the documentation in **epub**, **latex**, **json**, *&c* similarly.
|
||||
|
||||
.. _`reStructured Text`: http://docutils.sourceforge.net/rst.html
|
||||
.. _Sphinx: http://sphinx.pocoo.org
|
||||
.. _`GitHub Pages`: http://pages.github.com
|
||||
.. _Jeckyl: http://github.com/mojombo/jekyll
|
||||
.. _`sphinx-to-github`: http://github.com/michaeljones/sphinx-to-github
|
||||
|
||||
----------
|
||||
|
||||
Make sure to check out the :ref:`API Documentation <api>`.
|
||||
Make sure to check out the :ref:`API Documentation <api>`.
|
||||
|
||||
+52
-7
@@ -3,20 +3,20 @@
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Tablib: Pythonic Tabular Datasets
|
||||
Tablib: Pythonic Tabular Datasets
|
||||
=================================
|
||||
|
||||
Release |version|.
|
||||
Release v\ |version|. (:ref:`Installation <install>`)
|
||||
|
||||
.. Contents:
|
||||
..
|
||||
..
|
||||
.. .. toctree::
|
||||
.. :maxdepth: 2
|
||||
..
|
||||
..
|
||||
|
||||
.. Indices and tables
|
||||
.. ==================
|
||||
..
|
||||
..
|
||||
.. * :ref:`genindex`
|
||||
.. * :ref:`modindex`
|
||||
.. * :ref:`search`
|
||||
@@ -24,7 +24,52 @@ Release |version|.
|
||||
|
||||
Tablib is an :ref:`MIT Licensed <mit>` format-agnostic tabular dataset library, written in Python. It allows you to import, export, and manipulate tabular data sets. Advanced features include, segregation, dynamic columns, tags & filtering, and seamless format import & export.
|
||||
|
||||
I recommend you start with :ref:`Installation <install>`.
|
||||
::
|
||||
|
||||
>>> data = tablib.Dataset(headers=['First Name', 'Last Name', 'Age'])
|
||||
>>> for i in [('Kenneth', 'Reitz', 22), ('Bessie', 'Monke', 21)]:
|
||||
... data.append(i)
|
||||
|
||||
|
||||
>>> print(data.export('json'))
|
||||
[{"Last Name": "Reitz", "First Name": "Kenneth", "Age": 22}, {"Last Name": "Monke", "First Name": "Bessie", "Age": 21}]
|
||||
|
||||
>>> print(data.export('yaml'))
|
||||
- {Age: 22, First Name: Kenneth, Last Name: Reitz}
|
||||
- {Age: 21, First Name: Bessie, Last Name: Monke}
|
||||
|
||||
>>> data.export('xlsx')
|
||||
<censored binary data>
|
||||
|
||||
>>> data.export('df')
|
||||
First Name Last Name Age
|
||||
0 Kenneth Reitz 22
|
||||
1 Bessie Monke 21
|
||||
|
||||
|
||||
Testimonials
|
||||
------------
|
||||
|
||||
`National Geographic <http://www.nationalgeographic.com/>`_,
|
||||
`Digg, Inc <http://digg.com/>`_,
|
||||
`Northrop Grumman <http://www.northropgrumman.com/>`_,
|
||||
`Discovery Channel <http://dsc.discovery.com/>`_,
|
||||
and `The Sunlight Foundation <http://sunlightfoundation.com/>`_ use Tablib internally.
|
||||
|
||||
|
||||
|
||||
**Greg Thorton**
|
||||
Tablib by @kennethreitz saved my life. I had to consolidate like 5 huge poorly maintained lists of domains and data. It was a breeze!
|
||||
|
||||
**Dave Coutts**
|
||||
It's turning into one of my most used modules of 2010. You really hit a sweet spot for managing tabular data with a minimal amount of code and effort.
|
||||
|
||||
**Joshua Ourisman**
|
||||
Tablib has made it so much easier to deal with the inevitable 'I want an Excel file!' requests from clients...
|
||||
|
||||
**Brad Montgomery**
|
||||
I think you nailed the "Python Zen" with tablib. Thanks again for an awesome lib!
|
||||
|
||||
|
||||
User's Guide
|
||||
------------
|
||||
@@ -61,4 +106,4 @@ method, this part of the documentation is for you.
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
api
|
||||
api
|
||||
|
||||
+14
-25
@@ -2,7 +2,7 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
This part of the documentation covers the installation of Tablib. The first step to using any software package is getting it properly installed. Please read this section carefully, or you may miss out on some nice :ref:`speed enhancements <peed-extentions>`.
|
||||
This part of the documentation covers the installation of Tablib. The first step to using any software package is getting it properly installed.
|
||||
|
||||
|
||||
.. _installing:
|
||||
@@ -11,15 +11,12 @@ This part of the documentation covers the installation of Tablib. The first step
|
||||
Installing Tablib
|
||||
-----------------
|
||||
|
||||
To install Tablib, it only takes one simple command. ::
|
||||
Distribute & Pip
|
||||
----------------
|
||||
|
||||
$ pip install tablib
|
||||
Of course, the recommended way to install Tablib is with `pip <http://www.pip-installer.org/>`_::
|
||||
|
||||
Or, if you must: ::
|
||||
|
||||
$ easy_install tablib
|
||||
|
||||
But, you really shouldn't do that.
|
||||
$ pip install tablib
|
||||
|
||||
|
||||
-------------------
|
||||
@@ -43,36 +40,28 @@ To download the full source history from Git, see :ref:`Source Control <scm>`.
|
||||
.. _zipball: http://github.com/kennethreitz/tablib/zipball/master
|
||||
|
||||
|
||||
.. _speed-extentions:
|
||||
Speed Extentions
|
||||
.. _speed-extensions:
|
||||
Speed Extensions
|
||||
----------------
|
||||
|
||||
.. versionadded:: 0.8.5
|
||||
|
||||
Tablib is partially dependent on the **pyyaml**, **simplejson**, and **xlwt** modules. To reduce installation issues, fully integrated versions of all required libraries are included in Tablib.
|
||||
|
||||
However, if performance is important to you (and it should be), you can install **pyyaml** with C extentions from PyPi. ::
|
||||
|
||||
$ pip install PyYAML
|
||||
|
||||
If you're using Python 2.5, you should also install the **simplejson** module (pip will do this for you). If you're using Python 2.6+, the built-in **json** module is already optimized and in use. ::
|
||||
|
||||
$ pip install simplejson
|
||||
You can gain some speed improvement by optionally installing the ujson_ library.
|
||||
Tablib will fallback to the standard `json` module if it doesn't find ``ujson``.
|
||||
|
||||
.. _ujson: https://pypi.python.org/pypi/ujson
|
||||
|
||||
|
||||
.. _updates:
|
||||
Staying Updated
|
||||
---------------
|
||||
|
||||
The latest version of Tablib will always be available here:
|
||||
The latest version of Tablib will always be available here:
|
||||
|
||||
* PyPi: http://pypi.python.org/pypi/tablib/
|
||||
* GitHub: http://github.com/kennethreitz/tablib/
|
||||
|
||||
When a new version is available, upgrading is simple. ::
|
||||
When a new version is available, upgrading is simple::
|
||||
|
||||
$ pip install tablib --upgrade
|
||||
$ pip install tablib --upgrade
|
||||
|
||||
|
||||
Now, go get a :ref:`Quick Start <quickstart>`.
|
||||
Now, go get a :ref:`Quick Start <quickstart>`.
|
||||
|
||||
+22
-13
@@ -3,11 +3,14 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
This part of the documentation covers all the interfaces of Tablib.
|
||||
Tablib is a format-agnostic tabular dataset library, written in Python. It allows you to Pythonically import, export, and manipulate tabular data sets. Advanced features include, segregation, dynamic columns, tags / filtering, and seamless format import/export.
|
||||
This part of the documentation covers all the interfaces of Tablib.
|
||||
Tablib is a format-agnostic tabular dataset library, written in Python.
|
||||
It allows you to Pythonically import, export, and manipulate tabular data sets.
|
||||
Advanced features include, segregation, dynamic columns, tags / filtering, and
|
||||
seamless format import/export.
|
||||
|
||||
|
||||
Philosphy
|
||||
Philosophy
|
||||
---------
|
||||
|
||||
Tablib was developed with a few :pep:`20` idioms in mind.
|
||||
@@ -21,14 +24,19 @@ Tablib was developed with a few :pep:`20` idioms in mind.
|
||||
|
||||
All contributions to Tablib should keep these important rules in mind.
|
||||
|
||||
.. _mit:
|
||||
.. mit:
|
||||
|
||||
MIT License
|
||||
-----------
|
||||
|
||||
A large number of open source projects you find today are `GPL Licensed`_. While the GPL has its time and place, it should most certainly not be your go-to license for your next open source project.
|
||||
A large number of open source projects you find today are `GPL Licensed`_.
|
||||
While the GPL has its time and place, it should most certainly not be your
|
||||
go-to license for your next open source project.
|
||||
|
||||
A project that is released as GPL cannot be used in any commercial product without the product itself also being offered as open source. The MIT and BSD licenses are great alternatives to the GPL that allow your open-source software to be used in proprietary, closed-source software.
|
||||
A project that is released as GPL cannot be used in any commercial product
|
||||
without the product itself also being offered as open source. The MIT, BSD, and
|
||||
ISC licenses are great alternatives to the GPL that allow your open-source
|
||||
software to be used in proprietary, closed-source software.
|
||||
|
||||
Tablib is released under terms of `The MIT License`_.
|
||||
|
||||
@@ -41,7 +49,7 @@ Tablib is released under terms of `The MIT License`_.
|
||||
Tablib License
|
||||
--------------
|
||||
|
||||
Copyright (c) 2011 Kenneth Reitz.
|
||||
Copyright 2017 Kenneth Reitz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -67,18 +75,19 @@ THE SOFTWARE.
|
||||
Pythons Supported
|
||||
-----------------
|
||||
|
||||
At this time, the following Python platforms are officially supported:
|
||||
At this time, the following Python platforms are officially supported:
|
||||
|
||||
* cPython 2.5
|
||||
* cPython 2.6
|
||||
* cPython 2.7
|
||||
* cPython 3.1
|
||||
* cPython 3.2
|
||||
* cPython 3.3
|
||||
* cPython 3.4
|
||||
* cPython 3.5
|
||||
* cPython 3.6
|
||||
* PyPy-c 1.4
|
||||
* PyPy-c 1.5
|
||||
|
||||
Support for other Pythons will be rolled out soon.
|
||||
|
||||
|
||||
|
||||
|
||||
Now, go :ref:`Install Tablib <install>`.
|
||||
Now, go :ref:`Install Tablib <install>`.
|
||||
|
||||
+42
-17
@@ -39,6 +39,7 @@ You can now start filling this :class:`Dataset <tablib.Dataset>` object with dat
|
||||
|
||||
|
||||
|
||||
|
||||
-----------
|
||||
Adding Rows
|
||||
-----------
|
||||
@@ -68,7 +69,7 @@ Adding Headers
|
||||
--------------
|
||||
|
||||
|
||||
It's time enhance our :class:`Dataset` by giving our columns some titles. To do so, set :class:`Dataset.headers`. ::
|
||||
It's time to enhance our :class:`Dataset` by giving our columns some titles. To do so, set :class:`Dataset.headers`. ::
|
||||
|
||||
data.headers = ['First Name', 'Last Name']
|
||||
|
||||
@@ -87,7 +88,7 @@ Adding Columns
|
||||
|
||||
Now that we have a basic :class:`Dataset` in place, let's add a column of **ages** to it. ::
|
||||
|
||||
data.append(col=[22, 20], header='Age')
|
||||
data.append_col([22, 20], header='Age')
|
||||
|
||||
Let's view the data now. ::
|
||||
|
||||
@@ -97,6 +98,15 @@ Let's view the data now. ::
|
||||
It's that easy.
|
||||
|
||||
|
||||
--------------
|
||||
Importing Data
|
||||
--------------
|
||||
Creating a :class:`tablib.Dataset` object by importing a pre-existing file is simple. ::
|
||||
|
||||
imported_data = Dataset().load(open('data.csv').read())
|
||||
|
||||
This detects what sort of data is being passed in, and uses an appropriate formatter to do the import. So you can import from a variety of different file types.
|
||||
|
||||
--------------
|
||||
Exporting Data
|
||||
--------------
|
||||
@@ -105,30 +115,38 @@ Tablib's killer feature is the ability to export your :class:`Dataset` objects i
|
||||
|
||||
**Comma-Separated Values** ::
|
||||
|
||||
>>> data.csv
|
||||
>>> data.export('csv')
|
||||
Last Name,First Name,Age
|
||||
Reitz,Kenneth,22
|
||||
Monke,Bessie,20
|
||||
|
||||
**JavaScript Object Notation** ::
|
||||
|
||||
>>> data.json
|
||||
>>> data.export('json')
|
||||
[{"Last Name": "Reitz", "First Name": "Kenneth", "Age": 22}, {"Last Name": "Monke", "First Name": "Bessie", "Age": 20}]
|
||||
|
||||
|
||||
**YAML Ain't Markup Language** ::
|
||||
|
||||
>>> data.yaml
|
||||
>>> data.export('yaml')
|
||||
- {Age: 22, First Name: Kenneth, Last Name: Reitz}
|
||||
- {Age: 20, First Name: Bessie, Last Name: Monke}
|
||||
|
||||
|
||||
**Microsoft Excel** ::
|
||||
|
||||
>>> data.xls
|
||||
>>> data.export('xls')
|
||||
<censored binary data>
|
||||
|
||||
|
||||
**Pandas DataFrame** ::
|
||||
|
||||
>>> data.export('df')
|
||||
First Name Last Name Age
|
||||
0 Kenneth Reitz 22
|
||||
1 Bessie Monke 21
|
||||
|
||||
|
||||
------------------------
|
||||
Selecting Rows & Columns
|
||||
------------------------
|
||||
@@ -146,6 +164,13 @@ To do so, we access the :class:`Dataset` as if it were a standard Python diction
|
||||
>>> data['First Name']
|
||||
['Kenneth', 'Bessie']
|
||||
|
||||
You can also access the column using its index. ::
|
||||
|
||||
>>> data.headers
|
||||
['Last Name', 'First Name', 'Age']
|
||||
>>> data.get_col(1)
|
||||
['Kenneth', 'Bessie']
|
||||
|
||||
Let's find the average age. ::
|
||||
|
||||
>>> ages = data['Age']
|
||||
@@ -158,7 +183,7 @@ Let's find the average age. ::
|
||||
Removing Rows & Columns
|
||||
-----------------------
|
||||
|
||||
It's easier than you could imagine. ::
|
||||
It's easier than you could imagine::
|
||||
|
||||
>>> del data['Col Name']
|
||||
|
||||
@@ -195,11 +220,11 @@ Let's add a dynamic column to our :class:`Dataset` object. In this example, we h
|
||||
"""Returns a random integer for entry."""
|
||||
return (random.randint(60,100)/100.0)
|
||||
|
||||
data.append(col=[random_grade], header='Grade')
|
||||
data.append_col(random_grade, header='Grade')
|
||||
|
||||
Let's have a look at our data. ::
|
||||
|
||||
>>> data.yaml
|
||||
>>> data.export('yaml')
|
||||
- {Age: 22, First Name: Kenneth, Grade: 0.6, Last Name: Reitz}
|
||||
- {Age: 20, First Name: Bessie, Grade: 0.75, Last Name: Monke}
|
||||
|
||||
@@ -229,7 +254,7 @@ For example, we can use the data available in the row to guess the gender of a s
|
||||
|
||||
Adding this function to our dataset as a dynamic column would result in: ::
|
||||
|
||||
>>> data.yaml
|
||||
>>> data.export('yaml')
|
||||
- {Age: 22, First Name: Kenneth, Gender: Male, Last Name: Reitz}
|
||||
- {Age: 20, First Name: Bessie, Gender: Female, Last Name: Monke}
|
||||
|
||||
@@ -244,7 +269,7 @@ Filtering Datasets with Tags
|
||||
|
||||
|
||||
When constructing a :class:`Dataset` object, you can add tags to rows by specifying the ``tags`` parameter.
|
||||
This allows you to filter your :class:`Dataset` later. This can be useful so separate rows of data based on
|
||||
This allows you to filter your :class:`Dataset` later. This can be useful to separate rows of data based on
|
||||
arbitrary criteria (*e.g.* origin) that you don't want to include in your :class:`Dataset`.
|
||||
|
||||
Let's tag some students. ::
|
||||
@@ -253,13 +278,13 @@ Let's tag some students. ::
|
||||
|
||||
students.headers = ['first', 'last']
|
||||
|
||||
students.append(['Kenneth', 'Reitz'], tags=['male', 'technical'])
|
||||
students.append(['Bessie', 'Monke'], tags=['female', 'creative'])
|
||||
students.rpush(['Kenneth', 'Reitz'], tags=['male', 'technical'])
|
||||
students.rpush(['Bessie', 'Monke'], tags=['female', 'creative'])
|
||||
|
||||
Now that we have extra meta-data on our rows, we can use easily filter our :class:`Dataset`. Let's just see Male students. ::
|
||||
Now that we have extra meta-data on our rows, we can easily filter our :class:`Dataset`. Let's just see Male students. ::
|
||||
|
||||
|
||||
>>> data.filter(['male']).yaml
|
||||
>>> students.filter(['male']).yaml
|
||||
- {first: Kenneth, Last: Reitz}
|
||||
|
||||
It's that simple. The original :class:`Dataset` is untouched.
|
||||
@@ -273,7 +298,7 @@ When dealing with a large number of :class:`Datasets <Dataset>` in spreadsheet f
|
||||
|
||||
Let's say we have 3 different :class:`Datasets <Dataset>`. All we have to do is add then to a :class:`Databook` object... ::
|
||||
|
||||
book = tablib.Databook([data1, data2, data3])
|
||||
book = tablib.Databook((data1, data2, data3))
|
||||
|
||||
... and export to Excel just like :class:`Datasets <Dataset>`. ::
|
||||
|
||||
@@ -329,7 +354,7 @@ When, it's often useful to create a blank row containing information on the upco
|
||||
|
||||
# Write spreadsheet to disk
|
||||
with open('grades.xls', 'wb') as f:
|
||||
f.write(tests.xls)
|
||||
f.write(tests.export('xls'))
|
||||
|
||||
The resulting **tests.xls** will have the following layout:
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
certifi==2017.7.27.1
|
||||
chardet==3.0.4
|
||||
et-xmlfile==1.0.1
|
||||
idna==2.6
|
||||
jdcal==1.3
|
||||
numpy==1.13.1
|
||||
odfpy==1.3.5
|
||||
openpyxl==2.4.8
|
||||
pandas==0.20.3
|
||||
pkginfo==1.4.1
|
||||
python-dateutil==2.6.1
|
||||
pytz==2017.2
|
||||
PyYAML==3.12
|
||||
requests==2.18.4
|
||||
requests-toolbelt==0.8.0
|
||||
six==1.10.0
|
||||
tqdm==4.15.0
|
||||
unicodecsv==0.14.1
|
||||
urllib3==1.22
|
||||
xlrd==1.1.0
|
||||
xlwt==1.3.0
|
||||
@@ -2,54 +2,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from distutils.core import setup
|
||||
try:
|
||||
from setuptools import setup
|
||||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
|
||||
def publish():
|
||||
"""Publish to PyPi"""
|
||||
if sys.argv[-1] == 'publish':
|
||||
os.system("python setup.py sdist upload")
|
||||
|
||||
if sys.argv[-1] == "publish":
|
||||
publish()
|
||||
sys.exit()
|
||||
|
||||
required = []
|
||||
if sys.argv[-1] == 'speedups':
|
||||
try:
|
||||
__import__('pip')
|
||||
except ImportError:
|
||||
print('Pip required.')
|
||||
sys.exit(1)
|
||||
|
||||
if sys.version_info[:2] < (2,6):
|
||||
required.append('simplejson')
|
||||
os.system('pip install ujson')
|
||||
sys.exit()
|
||||
|
||||
if sys.argv[-1] == 'test':
|
||||
try:
|
||||
__import__('py')
|
||||
except ImportError:
|
||||
print('py.test required.')
|
||||
sys.exit(1)
|
||||
|
||||
errors = os.system('py.test test_tablib.py')
|
||||
sys.exit(bool(errors))
|
||||
|
||||
packages = [
|
||||
'tablib', 'tablib.formats',
|
||||
'tablib.packages',
|
||||
'tablib.packages.dbfpy',
|
||||
'tablib.packages.dbfpy3'
|
||||
]
|
||||
|
||||
install = [
|
||||
'odfpy',
|
||||
'openpyxl',
|
||||
'unicodecsv',
|
||||
'xlrd',
|
||||
'xlwt',
|
||||
'pyyaml',
|
||||
]
|
||||
|
||||
|
||||
with open('tablib/core.py', 'r') as fd:
|
||||
version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
|
||||
fd.read(), re.MULTILINE).group(1)
|
||||
|
||||
setup(
|
||||
name='tablib',
|
||||
version='0.9.7',
|
||||
version=version,
|
||||
description='Format agnostic tabular data library (XLS, JSON, YAML, CSV)',
|
||||
long_description=open('README.rst').read() + '\n\n' +
|
||||
open('HISTORY.rst').read(),
|
||||
long_description=(open('README.rst').read() + '\n\n' +
|
||||
open('HISTORY.rst').read()),
|
||||
author='Kenneth Reitz',
|
||||
author_email='me@kennethreitz.com',
|
||||
url='http://tablib.org',
|
||||
packages= [
|
||||
'tablib', 'tablib.formats',
|
||||
'tablib.packages',
|
||||
'tablib.packages.xlwt',
|
||||
'tablib.packages.openpyxl',
|
||||
'tablib.packages.yaml',
|
||||
'tablib.packages.unicodecsv'
|
||||
],
|
||||
install_requires=required,
|
||||
author_email='me@kennethreitz.org',
|
||||
url='http://python-tablib.org',
|
||||
packages=packages,
|
||||
license='MIT',
|
||||
classifiers=(
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
'Natural Language :: English',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.5',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.0',
|
||||
'Programming Language :: Python :: 3.1',
|
||||
'Programming Language :: Python :: 3.2',
|
||||
),
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
],
|
||||
tests_require=['pytest'],
|
||||
install_requires=install,
|
||||
extras_require={
|
||||
'pandas': ['pandas'],
|
||||
},
|
||||
)
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
""" Tablib.
|
||||
"""
|
||||
""" Tablib. """
|
||||
|
||||
from tablib.core import (
|
||||
Databook, Dataset, detect, import_set,
|
||||
InvalidDatasetType, InvalidDimensions, UnsupportedFormat
|
||||
Databook, Dataset, detect_format, import_set, import_book,
|
||||
InvalidDatasetType, InvalidDimensions, UnsupportedFormat,
|
||||
__version__
|
||||
)
|
||||
|
||||
|
||||
+10
-11
@@ -22,28 +22,27 @@ except ImportError:
|
||||
|
||||
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
|
||||
import tablib.packages.dbfpy3 as dbfpy
|
||||
|
||||
import csv
|
||||
from io import StringIO
|
||||
# py3 mappings
|
||||
|
||||
ifilter = filter
|
||||
xrange = range
|
||||
unicode = str
|
||||
bytes = bytes
|
||||
basestring = str
|
||||
xrange = range
|
||||
|
||||
else:
|
||||
from cStringIO import StringIO as BytesIO
|
||||
import tablib.packages.xlwt as xlwt
|
||||
from cStringIO import StringIO
|
||||
from tablib.packages import markup
|
||||
from itertools import ifilter
|
||||
from tablib.packages import openpyxl
|
||||
|
||||
# py2 mappings
|
||||
xrange = xrange
|
||||
import unicodecsv as csv
|
||||
import tablib.packages.dbfpy as dbfpy
|
||||
|
||||
unicode = unicode
|
||||
bytes = str
|
||||
basestring = basestring
|
||||
|
||||
|
||||
xrange = xrange
|
||||
|
||||
+552
-191
File diff suppressed because it is too large
Load Diff
@@ -10,5 +10,9 @@ from . import _yaml as yaml
|
||||
from . import _tsv as tsv
|
||||
from . import _html as html
|
||||
from . import _xlsx as xlsx
|
||||
from . import _ods as ods
|
||||
from . import _dbf as dbf
|
||||
from . import _latex as latex
|
||||
from . import _df as df
|
||||
|
||||
available = (json, xls, yaml, csv, tsv, html, xlsx)
|
||||
available = (json, xls, yaml, csv, dbf, tsv, html, latex, xlsx, ods, df)
|
||||
|
||||
+20
-33
@@ -1,42 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - CSV Support.
|
||||
""" Tablib - *SV Support.
|
||||
"""
|
||||
|
||||
import sys
|
||||
if sys.version_info[0] > 2:
|
||||
is_py3 = True
|
||||
|
||||
from io import StringIO
|
||||
import csv
|
||||
else:
|
||||
is_py3 = False
|
||||
from cStringIO import StringIO
|
||||
import tablib.packages.unicodecsv as csv
|
||||
|
||||
|
||||
|
||||
import os
|
||||
|
||||
import tablib
|
||||
from tablib.compat import is_py3, csv, StringIO
|
||||
|
||||
|
||||
title = 'csv'
|
||||
extentions = ('csv',)
|
||||
extensions = ('csv',)
|
||||
|
||||
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
DEFAULT_DELIMITER = ','
|
||||
|
||||
|
||||
|
||||
def export_set(dataset):
|
||||
def export_set(dataset, **kwargs):
|
||||
"""Returns CSV representation of Dataset."""
|
||||
stream = StringIO()
|
||||
|
||||
if is_py3:
|
||||
_csv = csv.writer(stream)
|
||||
else:
|
||||
_csv = csv.writer(stream, encoding=DEFAULT_ENCODING)
|
||||
kwargs.setdefault('delimiter', DEFAULT_DELIMITER)
|
||||
if not is_py3:
|
||||
kwargs.setdefault('encoding', DEFAULT_ENCODING)
|
||||
|
||||
_csv = csv.writer(stream, **kwargs)
|
||||
|
||||
for row in dataset._package(dicts=False):
|
||||
_csv.writerow(row)
|
||||
@@ -44,15 +30,16 @@ def export_set(dataset):
|
||||
return stream.getvalue()
|
||||
|
||||
|
||||
def import_set(dset, in_stream, headers=True):
|
||||
def import_set(dset, in_stream, headers=True, **kwargs):
|
||||
"""Returns dataset from CSV stream."""
|
||||
|
||||
dset.wipe()
|
||||
|
||||
if is_py3:
|
||||
rows = csv.reader(in_stream.splitlines())
|
||||
else:
|
||||
rows = csv.reader(in_stream.splitlines(), encoding=DEFAULT_ENCODING)
|
||||
kwargs.setdefault('delimiter', DEFAULT_DELIMITER)
|
||||
if not is_py3:
|
||||
kwargs.setdefault('encoding', DEFAULT_ENCODING)
|
||||
|
||||
rows = csv.reader(StringIO(in_stream), **kwargs)
|
||||
for i, row in enumerate(rows):
|
||||
|
||||
if (i == 0) and (headers):
|
||||
@@ -61,10 +48,10 @@ def import_set(dset, in_stream, headers=True):
|
||||
dset.append(row)
|
||||
|
||||
|
||||
def detect(stream):
|
||||
def detect(stream, delimiter=DEFAULT_DELIMITER):
|
||||
"""Returns True if given stream is valid CSV."""
|
||||
try:
|
||||
rows = dialect = csv.Sniffer().sniff(stream)
|
||||
csv.Sniffer().sniff(stream, delimiters=delimiter)
|
||||
return True
|
||||
except csv.Error:
|
||||
return False
|
||||
except (csv.Error, TypeError):
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - DBF Support.
|
||||
"""
|
||||
import tempfile
|
||||
import struct
|
||||
import os
|
||||
|
||||
from tablib.compat import StringIO
|
||||
from tablib.compat import dbfpy
|
||||
from tablib.compat import is_py3
|
||||
|
||||
if is_py3:
|
||||
from tablib.packages.dbfpy3 import dbf
|
||||
from tablib.packages.dbfpy3 import dbfnew
|
||||
from tablib.packages.dbfpy3 import record as dbfrecord
|
||||
import io
|
||||
else:
|
||||
from tablib.packages.dbfpy import dbf
|
||||
from tablib.packages.dbfpy import dbfnew
|
||||
from tablib.packages.dbfpy import record as dbfrecord
|
||||
|
||||
|
||||
title = 'dbf'
|
||||
extensions = ('csv',)
|
||||
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns DBF representation of a Dataset"""
|
||||
new_dbf = dbfnew.dbf_new()
|
||||
temp_file, temp_uri = tempfile.mkstemp()
|
||||
|
||||
# create the appropriate fields based on the contents of the first row
|
||||
first_row = dataset[0]
|
||||
for fieldname, field_value in zip(dataset.headers, first_row):
|
||||
if type(field_value) in [int, float]:
|
||||
new_dbf.add_field(fieldname, 'N', 10, 8)
|
||||
else:
|
||||
new_dbf.add_field(fieldname, 'C', 80)
|
||||
|
||||
new_dbf.write(temp_uri)
|
||||
|
||||
dbf_file = dbf.Dbf(temp_uri, readOnly=0)
|
||||
for row in dataset:
|
||||
record = dbfrecord.DbfRecord(dbf_file)
|
||||
for fieldname, field_value in zip(dataset.headers, row):
|
||||
record[fieldname] = field_value
|
||||
record.store()
|
||||
|
||||
dbf_file.close()
|
||||
dbf_stream = open(temp_uri, 'rb')
|
||||
if is_py3:
|
||||
stream = io.BytesIO(dbf_stream.read())
|
||||
else:
|
||||
stream = StringIO(dbf_stream.read())
|
||||
dbf_stream.close()
|
||||
os.close(temp_file)
|
||||
os.remove(temp_uri)
|
||||
return stream.getvalue()
|
||||
|
||||
def import_set(dset, in_stream, headers=True):
|
||||
"""Returns a dataset from a DBF stream."""
|
||||
|
||||
dset.wipe()
|
||||
if is_py3:
|
||||
_dbf = dbf.Dbf(io.BytesIO(in_stream))
|
||||
else:
|
||||
_dbf = dbf.Dbf(StringIO(in_stream))
|
||||
dset.headers = _dbf.fieldNames
|
||||
for record in range(_dbf.recordCount):
|
||||
row = [_dbf[record][f] for f in _dbf.fieldNames]
|
||||
dset.append(row)
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if the given stream is valid DBF"""
|
||||
#_dbf = dbf.Table(StringIO(stream))
|
||||
try:
|
||||
if is_py3:
|
||||
if type(stream) is not bytes:
|
||||
stream = bytes(stream, 'utf-8')
|
||||
_dbf = dbf.Dbf(io.BytesIO(stream), readOnly=True)
|
||||
else:
|
||||
_dbf = dbf.Dbf(StringIO(stream), readOnly=True)
|
||||
return True
|
||||
except (ValueError, struct.error):
|
||||
# When we try to open up a file that's not a DBF, dbfpy raises a
|
||||
# ValueError.
|
||||
# When unpacking a string argument with less than 8 chars, struct.error is
|
||||
# raised.
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
""" Tablib - DataFrame Support.
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
if sys.version_info[0] > 2:
|
||||
from io import BytesIO
|
||||
else:
|
||||
from cStringIO import StringIO as BytesIO
|
||||
|
||||
try:
|
||||
from pandas import DataFrame
|
||||
except ImportError:
|
||||
DataFrame = None
|
||||
|
||||
import tablib
|
||||
|
||||
from tablib.compat import unicode
|
||||
|
||||
title = 'df'
|
||||
extensions = ('df', )
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is a DataFrame."""
|
||||
if DataFrame is None:
|
||||
return False
|
||||
try:
|
||||
DataFrame(stream)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def export_set(dset, index=None):
|
||||
"""Returns DataFrame representation of DataBook."""
|
||||
if DataFrame is None:
|
||||
raise NotImplementedError(
|
||||
'DataFrame Format requires `pandas` to be installed.'
|
||||
' Try `pip install tablib[pandas]`.')
|
||||
dataframe = DataFrame(dset.dict, columns=dset.headers)
|
||||
return dataframe
|
||||
|
||||
|
||||
def import_set(dset, in_stream):
|
||||
"""Returns dataset from DataFrame."""
|
||||
dset.wipe()
|
||||
dset.dict = in_stream.to_dict(orient='records')
|
||||
+36
-26
@@ -5,56 +5,66 @@
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
if sys.version_info[0] > 2:
|
||||
from io import StringIO
|
||||
from io import BytesIO as StringIO
|
||||
from tablib.packages import markup3 as markup
|
||||
else:
|
||||
from cStringIO import StringIO
|
||||
from tablib.packages import markup
|
||||
|
||||
import tablib
|
||||
from tablib.compat import unicode
|
||||
import codecs
|
||||
|
||||
BOOK_ENDINGS = 'h3'
|
||||
|
||||
title = 'html'
|
||||
extentions = ('html', )
|
||||
extensions = ('html', )
|
||||
|
||||
|
||||
def export_set(dataset):
|
||||
"""HTML representation of a Dataset."""
|
||||
"""HTML representation of a Dataset."""
|
||||
|
||||
stream = StringIO()
|
||||
stream = StringIO()
|
||||
|
||||
page = markup.page()
|
||||
page.table.open()
|
||||
page = markup.page()
|
||||
page.table.open()
|
||||
|
||||
if dataset.headers is not None:
|
||||
page.thead.open()
|
||||
headers = markup.oneliner.th(dataset.headers)
|
||||
page.tr(headers)
|
||||
page.thead.close()
|
||||
if dataset.headers is not None:
|
||||
new_header = [item if item is not None else '' for item in dataset.headers]
|
||||
|
||||
for row in dataset:
|
||||
html_row = markup.oneliner.td(row)
|
||||
page.tr(html_row)
|
||||
page.thead.open()
|
||||
headers = markup.oneliner.th(new_header)
|
||||
page.tr(headers)
|
||||
page.thead.close()
|
||||
|
||||
page.table.close()
|
||||
for row in dataset:
|
||||
new_row = [item if item is not None else '' for item in row]
|
||||
|
||||
stream.writelines(str(page))
|
||||
html_row = markup.oneliner.td(new_row)
|
||||
page.tr(html_row)
|
||||
|
||||
return stream.getvalue()
|
||||
page.table.close()
|
||||
|
||||
# Allow unicode characters in output
|
||||
wrapper = codecs.getwriter("utf8")(stream)
|
||||
wrapper.writelines(unicode(page))
|
||||
|
||||
return stream.getvalue().decode('utf-8')
|
||||
|
||||
|
||||
def export_book(databook):
|
||||
"""HTML representation of a Databook."""
|
||||
"""HTML representation of a Databook."""
|
||||
|
||||
stream = StringIO()
|
||||
stream = StringIO()
|
||||
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
title = (dset.title if dset.title else 'Set %s' % (i))
|
||||
stream.write('<%s>%s</%s>\n' % (BOOK_ENDINGS, title, BOOK_ENDINGS))
|
||||
stream.write(dset.html)
|
||||
stream.write('\n')
|
||||
# Allow unicode characters in output
|
||||
wrapper = codecs.getwriter("utf8")(stream)
|
||||
|
||||
return stream.getvalue()
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
title = (dset.title if dset.title else 'Set %s' % (i))
|
||||
wrapper.write('<%s>%s</%s>\n' % (BOOK_ENDINGS, title, BOOK_ENDINGS))
|
||||
wrapper.write(dset.html)
|
||||
wrapper.write('\n')
|
||||
|
||||
return stream.getvalue().decode('utf-8')
|
||||
|
||||
+21
-13
@@ -2,43 +2,51 @@
|
||||
|
||||
""" Tablib - JSON Support
|
||||
"""
|
||||
import decimal
|
||||
|
||||
import tablib
|
||||
|
||||
import sys
|
||||
if sys.version_info[:2] > (2, 5):
|
||||
from tablib.packages import anyjson
|
||||
else:
|
||||
from tablib.packages import anyjson25 as anyjson
|
||||
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
title = 'json'
|
||||
extentions = ('json', 'jsn')
|
||||
extensions = ('json', 'jsn')
|
||||
|
||||
|
||||
def date_handler(obj):
|
||||
if isinstance(obj, decimal.Decimal):
|
||||
return str(obj)
|
||||
elif hasattr(obj, 'isoformat'):
|
||||
return obj.isoformat()
|
||||
else:
|
||||
return obj
|
||||
# return obj.isoformat() if hasattr(obj, 'isoformat') else obj
|
||||
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns JSON representation of Dataset."""
|
||||
return anyjson.serialize(dataset.dict)
|
||||
return json.dumps(dataset.dict, default=date_handler)
|
||||
|
||||
|
||||
def export_book(databook):
|
||||
"""Returns JSON representation of Databook."""
|
||||
return anyjson.serialize(databook._package())
|
||||
return json.dumps(databook._package(), default=date_handler)
|
||||
|
||||
|
||||
def import_set(dset, in_stream):
|
||||
"""Returns dataset from JSON stream."""
|
||||
|
||||
dset.wipe()
|
||||
dset.dict = anyjson.deserialize(in_stream)
|
||||
dset.dict = json.loads(in_stream)
|
||||
|
||||
|
||||
def import_book(dbook, in_stream):
|
||||
"""Returns databook from JSON stream."""
|
||||
|
||||
dbook.wipe()
|
||||
for sheet in anyjson.deserialize(in_stream):
|
||||
for sheet in json.loads(in_stream):
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet['title']
|
||||
data.dict = sheet['data']
|
||||
@@ -48,7 +56,7 @@ def import_book(dbook, in_stream):
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is valid JSON."""
|
||||
try:
|
||||
anyjson.deserialize(stream)
|
||||
json.loads(stream)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Tablib - LaTeX table export support.
|
||||
|
||||
Generates a LaTeX booktabs-style table from the dataset.
|
||||
"""
|
||||
import re
|
||||
|
||||
from tablib.compat import unicode
|
||||
|
||||
title = 'latex'
|
||||
extensions = ('tex',)
|
||||
|
||||
TABLE_TEMPLATE = """\
|
||||
%% Note: add \\usepackage{booktabs} to your preamble
|
||||
%%
|
||||
\\begin{table}[!htbp]
|
||||
\\centering
|
||||
%(CAPTION)s
|
||||
\\begin{tabular}{%(COLSPEC)s}
|
||||
\\toprule
|
||||
%(HEADER)s
|
||||
%(MIDRULE)s
|
||||
%(BODY)s
|
||||
\\bottomrule
|
||||
\\end{tabular}
|
||||
\\end{table}
|
||||
"""
|
||||
|
||||
TEX_RESERVED_SYMBOLS_MAP = dict([
|
||||
('\\', '\\textbackslash{}'),
|
||||
('{', '\\{'),
|
||||
('}', '\\}'),
|
||||
('$', '\\$'),
|
||||
('&', '\\&'),
|
||||
('#', '\\#'),
|
||||
('^', '\\textasciicircum{}'),
|
||||
('_', '\\_'),
|
||||
('~', '\\textasciitilde{}'),
|
||||
('%', '\\%'),
|
||||
])
|
||||
|
||||
TEX_RESERVED_SYMBOLS_RE = re.compile(
|
||||
'(%s)' % '|'.join(map(re.escape, TEX_RESERVED_SYMBOLS_MAP.keys())))
|
||||
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns LaTeX representation of dataset
|
||||
|
||||
:param dataset: dataset to serialize
|
||||
:type dataset: tablib.core.Dataset
|
||||
"""
|
||||
|
||||
caption = '\\caption{%s}' % dataset.title if dataset.title else '%'
|
||||
colspec = _colspec(dataset.width)
|
||||
header = _serialize_row(dataset.headers) if dataset.headers else ''
|
||||
midrule = _midrule(dataset.width)
|
||||
body = '\n'.join([_serialize_row(row) for row in dataset])
|
||||
return TABLE_TEMPLATE % dict(CAPTION=caption, COLSPEC=colspec,
|
||||
HEADER=header, MIDRULE=midrule, BODY=body)
|
||||
|
||||
|
||||
def _colspec(dataset_width):
|
||||
"""Generates the column specification for the LaTeX `tabular` environment
|
||||
based on the dataset width.
|
||||
|
||||
The first column is justified to the left, all further columns are aligned
|
||||
to the right.
|
||||
|
||||
.. note:: This is only a heuristic and most probably has to be fine-tuned
|
||||
post export. Column alignment should depend on the data type, e.g., textual
|
||||
content should usually be aligned to the left while numeric content almost
|
||||
always should be aligned to the right.
|
||||
|
||||
:param dataset_width: width of the dataset
|
||||
"""
|
||||
|
||||
spec = 'l'
|
||||
for _ in range(1, dataset_width):
|
||||
spec += 'r'
|
||||
return spec
|
||||
|
||||
|
||||
def _midrule(dataset_width):
|
||||
"""Generates the table `midrule`, which may be composed of several
|
||||
`cmidrules`.
|
||||
|
||||
:param dataset_width: width of the dataset to serialize
|
||||
"""
|
||||
|
||||
if not dataset_width or dataset_width == 1:
|
||||
return '\\midrule'
|
||||
return ' '.join([_cmidrule(colindex, dataset_width) for colindex in
|
||||
range(1, dataset_width + 1)])
|
||||
|
||||
|
||||
def _cmidrule(colindex, dataset_width):
|
||||
"""Generates the `cmidrule` for a single column with appropriate trimming
|
||||
based on the column position.
|
||||
|
||||
:param colindex: Column index
|
||||
:param dataset_width: width of the dataset
|
||||
"""
|
||||
|
||||
rule = '\\cmidrule(%s){%d-%d}'
|
||||
if colindex == 1:
|
||||
# Rule of first column is trimmed on the right
|
||||
return rule % ('r', colindex, colindex)
|
||||
if colindex == dataset_width:
|
||||
# Rule of last column is trimmed on the left
|
||||
return rule % ('l', colindex, colindex)
|
||||
# Inner columns are trimmed on the left and right
|
||||
return rule % ('lr', colindex, colindex)
|
||||
|
||||
|
||||
def _serialize_row(row):
|
||||
"""Returns string representation of a single row.
|
||||
|
||||
:param row: single dataset row
|
||||
"""
|
||||
|
||||
new_row = [_escape_tex_reserved_symbols(unicode(item)) if item else '' for
|
||||
item in row]
|
||||
return 6 * ' ' + ' & '.join(new_row) + ' \\\\'
|
||||
|
||||
|
||||
def _escape_tex_reserved_symbols(input):
|
||||
"""Escapes all TeX reserved symbols ('_', '~', etc.) in a string.
|
||||
|
||||
:param input: String to escape
|
||||
"""
|
||||
def replace(match):
|
||||
return TEX_RESERVED_SYMBOLS_MAP[match.group()]
|
||||
return TEX_RESERVED_SYMBOLS_RE.sub(replace, input)
|
||||
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Tablib - ODF Support.
|
||||
"""
|
||||
|
||||
from odf import opendocument, style, table, text
|
||||
from tablib.compat import BytesIO, unicode
|
||||
|
||||
title = 'ods'
|
||||
extensions = ('ods',)
|
||||
|
||||
bold = style.Style(name="bold", family="paragraph")
|
||||
bold.addElement(style.TextProperties(fontweight="bold", fontweightasian="bold", fontweightcomplex="bold"))
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns ODF representation of Dataset."""
|
||||
|
||||
wb = opendocument.OpenDocumentSpreadsheet()
|
||||
wb.automaticstyles.addElement(bold)
|
||||
|
||||
ws = table.Table(name=dataset.title if dataset.title else 'Tablib Dataset')
|
||||
wb.spreadsheet.addElement(ws)
|
||||
dset_sheet(dataset, ws)
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
|
||||
def export_book(databook):
|
||||
"""Returns ODF representation of DataBook."""
|
||||
|
||||
wb = opendocument.OpenDocumentSpreadsheet()
|
||||
wb.automaticstyles.addElement(bold)
|
||||
|
||||
for i, dset in enumerate(databook._datasets):
|
||||
ws = table.Table(name=dset.title if dset.title else 'Sheet%s' % (i))
|
||||
wb.spreadsheet.addElement(ws)
|
||||
dset_sheet(dset, ws)
|
||||
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
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
|
||||
odf_row = table.TableRow(stylename=bold, defaultcellstylename='bold')
|
||||
for j, col in enumerate(row):
|
||||
try:
|
||||
col = unicode(col, errors='ignore')
|
||||
except TypeError:
|
||||
## col is already unicode
|
||||
pass
|
||||
ws.addElement(table.TableColumn())
|
||||
|
||||
# bold headers
|
||||
if (row_number == 1) and dataset.headers:
|
||||
odf_row.setAttribute('stylename', bold)
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
p = text.P()
|
||||
p.addElement(text.Span(text=col, stylename=bold))
|
||||
cell.addElement(p)
|
||||
odf_row.addElement(cell)
|
||||
|
||||
# wrap the rest
|
||||
else:
|
||||
try:
|
||||
if '\n' in col:
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
cell.addElement(text.P(text=col))
|
||||
odf_row.addElement(cell)
|
||||
else:
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
cell.addElement(text.P(text=col))
|
||||
odf_row.addElement(cell)
|
||||
except TypeError:
|
||||
ws.addElement(odf_row)
|
||||
cell = table.TableCell()
|
||||
cell.addElement(text.P(text=col))
|
||||
odf_row.addElement(cell)
|
||||
+12
-38
@@ -3,54 +3,28 @@
|
||||
""" Tablib - TSV (Tab Separated Values) Support.
|
||||
"""
|
||||
|
||||
import sys
|
||||
if sys.version_info[0] > 2:
|
||||
from io import StringIO
|
||||
else:
|
||||
from cStringIO import StringIO
|
||||
|
||||
import csv
|
||||
import os
|
||||
|
||||
import tablib
|
||||
|
||||
from tablib.formats._csv import (
|
||||
export_set as export_set_wrapper,
|
||||
import_set as import_set_wrapper,
|
||||
detect as detect_wrapper,
|
||||
)
|
||||
|
||||
title = 'tsv'
|
||||
extentions = ('tsv',)
|
||||
|
||||
extensions = ('tsv',)
|
||||
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
DELIMITER = '\t'
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns a TSV representation of Dataset."""
|
||||
stream = StringIO()
|
||||
_tsv = csv.writer(stream, delimiter='\t')
|
||||
|
||||
for row in dataset._package(dicts=False):
|
||||
_tsv.writerow(row)
|
||||
|
||||
return stream.getvalue()
|
||||
"""Returns TSV representation of Dataset."""
|
||||
return export_set_wrapper(dataset, delimiter=DELIMITER)
|
||||
|
||||
|
||||
def import_set(dset, in_stream, headers=True):
|
||||
"""Returns dataset from TSV stream."""
|
||||
dset.wipe()
|
||||
|
||||
rows = csv.reader(in_stream.split('\r\n'), delimiter='\t')
|
||||
for i, row in enumerate(rows):
|
||||
# Skip empty rows
|
||||
if not row:
|
||||
continue
|
||||
|
||||
if (i == 0) and (headers):
|
||||
dset.headers = row
|
||||
else:
|
||||
dset.append(row)
|
||||
return import_set_wrapper(dset, in_stream, headers=headers, delimiter=DELIMITER)
|
||||
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is valid TSV."""
|
||||
try:
|
||||
rows = dialect = csv.Sniffer().sniff(stream, delimiters='\t')
|
||||
return True
|
||||
except csv.Error:
|
||||
return False
|
||||
return detect_wrapper(stream, delimiter=DELIMITER)
|
||||
|
||||
+61
-5
@@ -5,17 +5,39 @@
|
||||
|
||||
import sys
|
||||
|
||||
from tablib.compat import BytesIO, xlwt
|
||||
|
||||
from tablib.compat import BytesIO, xrange
|
||||
import tablib
|
||||
import xlrd
|
||||
import xlwt
|
||||
from xlrd.biffh import XLRDError
|
||||
|
||||
title = 'xls'
|
||||
extentions = ('xls',)
|
||||
extensions = ('xls',)
|
||||
|
||||
# special styles
|
||||
wrap = xlwt.easyxf("alignment: wrap on")
|
||||
bold = xlwt.easyxf("font: bold on")
|
||||
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is a readable excel file."""
|
||||
try:
|
||||
xlrd.open_workbook(file_contents=stream)
|
||||
return True
|
||||
except (TypeError, XLRDError):
|
||||
pass
|
||||
try:
|
||||
xlrd.open_workbook(file_contents=stream.read())
|
||||
return True
|
||||
except (AttributeError, XLRDError):
|
||||
pass
|
||||
try:
|
||||
xlrd.open_workbook(filename=stream)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns XLS representation of Dataset."""
|
||||
|
||||
@@ -45,6 +67,42 @@ def export_book(databook):
|
||||
return stream.getvalue()
|
||||
|
||||
|
||||
def import_set(dset, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
dset.wipe()
|
||||
|
||||
xls_book = xlrd.open_workbook(file_contents=in_stream)
|
||||
sheet = xls_book.sheet_by_index(0)
|
||||
|
||||
dset.title = sheet.name
|
||||
|
||||
for i in xrange(sheet.nrows):
|
||||
if (i == 0) and (headers):
|
||||
dset.headers = sheet.row_values(0)
|
||||
else:
|
||||
dset.append(sheet.row_values(i))
|
||||
|
||||
def import_book(dbook, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
dbook.wipe()
|
||||
|
||||
xls_book = xlrd.open_workbook(file_contents=in_stream)
|
||||
|
||||
for sheet in xls_book.sheets():
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet.name
|
||||
|
||||
for i in xrange(sheet.nrows):
|
||||
if (i == 0) and (headers):
|
||||
data.headers = sheet.row_values(0)
|
||||
else:
|
||||
data.append(sheet.row_values(i))
|
||||
|
||||
dbook.add_sheet(data)
|
||||
|
||||
|
||||
def dset_sheet(dataset, ws):
|
||||
"""Completes given worksheet from given Dataset."""
|
||||
_package = dataset._package(dicts=False)
|
||||
@@ -78,5 +136,3 @@ def dset_sheet(dataset, ws):
|
||||
ws.write(i, j, col)
|
||||
except TypeError:
|
||||
ws.write(i, j, col)
|
||||
|
||||
|
||||
|
||||
+77
-29
@@ -11,50 +11,100 @@ if sys.version_info[0] > 2:
|
||||
else:
|
||||
from cStringIO import StringIO as BytesIO
|
||||
|
||||
from tablib.compat import openpyxl
|
||||
import openpyxl
|
||||
import tablib
|
||||
|
||||
Workbook = openpyxl.workbook.Workbook
|
||||
ExcelWriter = openpyxl.writer.excel.ExcelWriter
|
||||
get_column_letter = openpyxl.cell.get_column_letter
|
||||
get_column_letter = openpyxl.utils.get_column_letter
|
||||
|
||||
from tablib.compat import unicode
|
||||
|
||||
|
||||
title = 'xlsx'
|
||||
extentions = ('xlsx',)
|
||||
extensions = ('xlsx',)
|
||||
|
||||
def export_set(dataset):
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is a readable excel file."""
|
||||
try:
|
||||
openpyxl.reader.excel.load_workbook(stream)
|
||||
return True
|
||||
except openpyxl.shared.exc.InvalidFileException:
|
||||
pass
|
||||
|
||||
def export_set(dataset, freeze_panes=True):
|
||||
"""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)
|
||||
dset_sheet(dataset, ws, freeze_panes=freeze_panes)
|
||||
|
||||
stream = BytesIO()
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
|
||||
def export_book(databook):
|
||||
def export_book(databook, freeze_panes=True):
|
||||
"""Returns XLSX representation of DataBook."""
|
||||
|
||||
wb = Workbook()
|
||||
ew = ExcelWriter(workbook = wb)
|
||||
for sheet in wb.worksheets:
|
||||
wb.remove_sheet(sheet)
|
||||
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)
|
||||
dset_sheet(dset, ws, freeze_panes=freeze_panes)
|
||||
|
||||
|
||||
stream = BytesIO()
|
||||
ew.save(stream)
|
||||
wb.save(stream)
|
||||
return stream.getvalue()
|
||||
|
||||
|
||||
def dset_sheet(dataset, ws):
|
||||
def import_set(dset, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
dset.wipe()
|
||||
|
||||
xls_book = openpyxl.reader.excel.load_workbook(BytesIO(in_stream))
|
||||
sheet = xls_book.get_active_sheet()
|
||||
|
||||
dset.title = sheet.title
|
||||
|
||||
for i, row in enumerate(sheet.rows):
|
||||
row_vals = [c.value for c in row]
|
||||
if (i == 0) and (headers):
|
||||
dset.headers = row_vals
|
||||
else:
|
||||
dset.append(row_vals)
|
||||
|
||||
|
||||
def import_book(dbook, in_stream, headers=True):
|
||||
"""Returns databook from XLS stream."""
|
||||
|
||||
dbook.wipe()
|
||||
|
||||
xls_book = openpyxl.reader.excel.load_workbook(BytesIO(in_stream))
|
||||
|
||||
for sheet in xls_book.worksheets:
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet.title
|
||||
|
||||
for i, row in enumerate(sheet.rows):
|
||||
row_vals = [c.value for c in row]
|
||||
if (i == 0) and (headers):
|
||||
data.headers = row_vals
|
||||
else:
|
||||
data.append(row_vals)
|
||||
|
||||
dbook.add_sheet(data)
|
||||
|
||||
|
||||
def dset_sheet(dataset, ws, freeze_panes=True):
|
||||
"""Completes given worksheet from given Dataset."""
|
||||
_package = dataset._package(dicts=False)
|
||||
|
||||
@@ -62,40 +112,38 @@ def dset_sheet(dataset, ws):
|
||||
_offset = i
|
||||
_package.insert((sep[0] + _offset), (sep[1],))
|
||||
|
||||
bold = openpyxl.styles.Font(bold=True)
|
||||
wrap_text = openpyxl.styles.Alignment(wrap_text=True)
|
||||
|
||||
for i, row in enumerate(_package):
|
||||
row_number = i + 1
|
||||
for j, col in enumerate(row):
|
||||
col_idx = get_column_letter(j + 1)
|
||||
cell = ws.cell('%s%s' % (col_idx, row_number))
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
# cell.value = unicode('%s' % col, errors='ignore')
|
||||
cell.value = unicode(col)
|
||||
cell.font = bold
|
||||
if freeze_panes:
|
||||
# Export Freeze only after first Line
|
||||
ws.freeze_panes = 'A2'
|
||||
|
||||
# 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
|
||||
cell.value = unicode('%s' % col, errors='ignore')
|
||||
cell.font = bold
|
||||
|
||||
# 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
|
||||
cell.value = unicode('%s' % col, errors='ignore')
|
||||
cell.alignment = wrap_text
|
||||
else:
|
||||
ws.cell('%s%s'%(col_idx, row_number)).value = unicode(
|
||||
'%s' % col, errors='ignore')
|
||||
cell.value = unicode('%s' % col, errors='ignore')
|
||||
except TypeError:
|
||||
ws.cell('%s%s'%(col_idx, row_number)).value = unicode(col)
|
||||
cell.value = unicode(col)
|
||||
|
||||
|
||||
|
||||
+13
-23
@@ -3,41 +3,29 @@
|
||||
""" Tablib - YAML Support.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
if sys.version_info[0] > 2:
|
||||
import tablib.packages.yaml3 as yaml
|
||||
else:
|
||||
import tablib.packages.yaml as yaml
|
||||
|
||||
|
||||
import tablib
|
||||
|
||||
|
||||
import yaml
|
||||
|
||||
title = 'yaml'
|
||||
extentions = ('yaml', 'yml')
|
||||
|
||||
extensions = ('yaml', 'yml')
|
||||
|
||||
|
||||
def export_set(dataset):
|
||||
"""Returns YAML representation of Dataset."""
|
||||
return yaml.dump(dataset.dict)
|
||||
|
||||
return yaml.safe_dump(dataset._package(ordered=False))
|
||||
|
||||
|
||||
def export_book(databook):
|
||||
"""Returns YAML representation of Databook."""
|
||||
return yaml.dump(databook._package())
|
||||
return yaml.safe_dump(databook._package(ordered=False))
|
||||
|
||||
|
||||
def import_set(dset, in_stream):
|
||||
"""Returns dataset from YAML stream."""
|
||||
|
||||
dset.wipe()
|
||||
dset.dict = yaml.load(in_stream)
|
||||
dset.dict = yaml.safe_load(in_stream)
|
||||
|
||||
|
||||
def import_book(dbook, in_stream):
|
||||
@@ -45,19 +33,21 @@ def import_book(dbook, in_stream):
|
||||
|
||||
dbook.wipe()
|
||||
|
||||
for sheet in yaml.load(in_stream):
|
||||
for sheet in yaml.safe_load(in_stream):
|
||||
data = tablib.Dataset()
|
||||
data.title = sheet['title']
|
||||
data.dict = sheet['data']
|
||||
dbook.add_sheet(data)
|
||||
|
||||
|
||||
|
||||
def detect(stream):
|
||||
"""Returns True if given stream is valid YAML."""
|
||||
try:
|
||||
_yaml = yaml.load(stream)
|
||||
_yaml = yaml.safe_load(stream)
|
||||
if isinstance(_yaml, (list, tuple, dict)):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except yaml.parser.ParserError:
|
||||
return False
|
||||
except (yaml.parser.ParserError, yaml.reader.ReaderError,
|
||||
yaml.scanner.ScannerError):
|
||||
return False
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
"""
|
||||
Wraps the best available JSON implementation available in a common interface
|
||||
"""
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__author__ = "Rune Halvorsen <runefh@gmail.com>"
|
||||
__homepage__ = "http://bitbucket.org/runeh/anyjson/"
|
||||
__docformat__ = "restructuredtext"
|
||||
|
||||
"""
|
||||
|
||||
.. function:: serialize(obj)
|
||||
|
||||
Serialize the object to JSON.
|
||||
|
||||
.. function:: deserialize(str)
|
||||
|
||||
Deserialize JSON-encoded object to a Python object.
|
||||
|
||||
.. function:: force_implementation(name)
|
||||
|
||||
Load a specific json module. This is useful for testing and not much else
|
||||
|
||||
.. attribute:: implementation
|
||||
|
||||
The json implementation object. This is probably not useful to you,
|
||||
except to get the name of the implementation in use. The name is
|
||||
available through `implementation.name`.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
implementation = None
|
||||
|
||||
"""
|
||||
.. data:: _modules
|
||||
|
||||
List of known json modules, and the names of their serialize/unserialize
|
||||
methods, as well as the exception they throw. Exception can be either
|
||||
an exception class or a string.
|
||||
"""
|
||||
_modules = [("cjson", "encode", "EncodeError", "decode", "DecodeError"),
|
||||
("jsonlib2", "write", "WriteError", "read", "ReadError"),
|
||||
("jsonlib", "write", "WriteError", "read", "ReadError"),
|
||||
("simplejson", "dumps", TypeError, "loads", ValueError),
|
||||
("json", "dumps", TypeError, "loads", ValueError),
|
||||
("django.utils.simplejson", "dumps", TypeError, "loads",
|
||||
ValueError)]
|
||||
_fields = ("modname", "encoder", "encerror", "decoder", "decerror")
|
||||
|
||||
|
||||
class _JsonImplementation(object):
|
||||
"""Incapsulates a JSON implementation"""
|
||||
|
||||
def __init__(self, modspec):
|
||||
modinfo = dict(list(zip(_fields, modspec)))
|
||||
|
||||
# No try block. We want importerror to end up at caller
|
||||
module = self._attempt_load(modinfo["modname"])
|
||||
|
||||
self.implementation = modinfo["modname"]
|
||||
self._encode = getattr(module, modinfo["encoder"])
|
||||
self._decode = getattr(module, modinfo["decoder"])
|
||||
self._encode_error = modinfo["encerror"]
|
||||
self._decode_error = modinfo["decerror"]
|
||||
|
||||
if isinstance(modinfo["encerror"], str):
|
||||
self._encode_error = getattr(module, modinfo["encerror"])
|
||||
if isinstance(modinfo["decerror"], str):
|
||||
self._decode_error = getattr(module, modinfo["decerror"])
|
||||
|
||||
self.name = modinfo["modname"]
|
||||
|
||||
def _attempt_load(self, modname):
|
||||
"""Attempt to load module name modname, returning it on success,
|
||||
throwing ImportError if module couldn't be imported"""
|
||||
__import__(modname)
|
||||
return sys.modules[modname]
|
||||
|
||||
def serialize(self, data):
|
||||
"""Serialize the datastructure to json. Returns a string. Raises
|
||||
TypeError if the object could not be serialized."""
|
||||
try:
|
||||
return self._encode(data)
|
||||
except self._encode_error as exc:
|
||||
raise TypeError(*exc.args)
|
||||
|
||||
def deserialize(self, s):
|
||||
"""deserialize the string to python data types. Raises
|
||||
ValueError if the string vould not be parsed."""
|
||||
try:
|
||||
return self._decode(s)
|
||||
except self._decode_error as exc:
|
||||
raise ValueError(*exc.args)
|
||||
|
||||
|
||||
def force_implementation(modname):
|
||||
"""Forces anyjson to use a specific json module if it's available"""
|
||||
global implementation
|
||||
for name, spec in [(e[0], e) for e in _modules]:
|
||||
if name == modname:
|
||||
implementation = _JsonImplementation(spec)
|
||||
return
|
||||
raise ImportError("No module named: %s" % modname)
|
||||
|
||||
|
||||
for modspec in _modules:
|
||||
try:
|
||||
implementation = _JsonImplementation(modspec)
|
||||
break
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
raise ImportError("No supported JSON module found")
|
||||
|
||||
serialize = lambda value: implementation.serialize(value)
|
||||
deserialize = lambda value: implementation.deserialize(value)
|
||||
@@ -1,118 +0,0 @@
|
||||
u"""
|
||||
Wraps the best available JSON implementation available in a common interface
|
||||
"""
|
||||
|
||||
__version__ = u"0.2.0"
|
||||
__author__ = u"Rune Halvorsen <runefh@gmail.com>"
|
||||
__homepage__ = u"http://bitbucket.org/runeh/anyjson/"
|
||||
__docformat__ = u"restructuredtext"
|
||||
|
||||
u"""
|
||||
|
||||
.. function:: serialize(obj)
|
||||
|
||||
Serialize the object to JSON.
|
||||
|
||||
.. function:: deserialize(str)
|
||||
|
||||
Deserialize JSON-encoded object to a Python object.
|
||||
|
||||
.. function:: force_implementation(name)
|
||||
|
||||
Load a specific json module. This is useful for testing and not much else
|
||||
|
||||
.. attribute:: implementation
|
||||
|
||||
The json implementation object. This is probably not useful to you,
|
||||
except to get the name of the implementation in use. The name is
|
||||
available through `implementation.name`.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from itertools import izip
|
||||
|
||||
implementation = None
|
||||
|
||||
u"""
|
||||
.. data:: _modules
|
||||
|
||||
List of known json modules, and the names of their serialize/unserialize
|
||||
methods, as well as the exception they throw. Exception can be either
|
||||
an exception class or a string.
|
||||
"""
|
||||
_modules = [(u"cjson", u"encode", u"EncodeError", u"decode", u"DecodeError"),
|
||||
(u"jsonlib2", u"write", u"WriteError", u"read", u"ReadError"),
|
||||
(u"jsonlib", u"write", u"WriteError", u"read", u"ReadError"),
|
||||
(u"simplejson", u"dumps", TypeError, u"loads", ValueError),
|
||||
(u"json", u"dumps", TypeError, u"loads", ValueError),
|
||||
(u"django.utils.simplejson", u"dumps", TypeError, u"loads",
|
||||
ValueError)]
|
||||
_fields = (u"modname", u"encoder", u"encerror", u"decoder", u"decerror")
|
||||
|
||||
|
||||
class _JsonImplementation(object):
|
||||
u"""Incapsulates a JSON implementation"""
|
||||
|
||||
def __init__(self, modspec):
|
||||
modinfo = dict(list(izip(_fields, modspec)))
|
||||
|
||||
# No try block. We want importerror to end up at caller
|
||||
module = self._attempt_load(modinfo[u"modname"])
|
||||
|
||||
self.implementation = modinfo[u"modname"]
|
||||
self._encode = getattr(module, modinfo[u"encoder"])
|
||||
self._decode = getattr(module, modinfo[u"decoder"])
|
||||
self._encode_error = modinfo[u"encerror"]
|
||||
self._decode_error = modinfo[u"decerror"]
|
||||
|
||||
if isinstance(modinfo[u"encerror"], unicode):
|
||||
self._encode_error = getattr(module, modinfo[u"encerror"])
|
||||
if isinstance(modinfo[u"decerror"], unicode):
|
||||
self._decode_error = getattr(module, modinfo[u"decerror"])
|
||||
|
||||
self.name = modinfo[u"modname"]
|
||||
|
||||
def _attempt_load(self, modname):
|
||||
u"""Attempt to load module name modname, returning it on success,
|
||||
throwing ImportError if module couldn't be imported"""
|
||||
__import__(modname)
|
||||
return sys.modules[modname]
|
||||
|
||||
def serialize(self, data):
|
||||
u"""Serialize the datastructure to json. Returns a string. Raises
|
||||
TypeError if the object could not be serialized."""
|
||||
try:
|
||||
return self._encode(data)
|
||||
except self._encode_error, exc:
|
||||
raise TypeError(*exc.args)
|
||||
|
||||
def deserialize(self, s):
|
||||
u"""deserialize the string to python data types. Raises
|
||||
ValueError if the string vould not be parsed."""
|
||||
try:
|
||||
return self._decode(s)
|
||||
except self._decode_error, exc:
|
||||
raise ValueError(*exc.args)
|
||||
|
||||
|
||||
def force_implementation(modname):
|
||||
u"""Forces anyjson to use a specific json module if it's available"""
|
||||
global implementation
|
||||
for name, spec in [(e[0], e) for e in _modules]:
|
||||
if name == modname:
|
||||
implementation = _JsonImplementation(spec)
|
||||
return
|
||||
raise ImportError(u"No module named: %s" % modname)
|
||||
|
||||
|
||||
for modspec in _modules:
|
||||
try:
|
||||
implementation = _JsonImplementation(modspec)
|
||||
break
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
raise ImportError(u"No supported JSON module found")
|
||||
|
||||
serialize = lambda value: implementation.serialize(value)
|
||||
deserialize = lambda value: implementation.deserialize(value)
|
||||
@@ -0,0 +1,297 @@
|
||||
#! /usr/bin/env python
|
||||
"""DBF accessing helpers.
|
||||
|
||||
FIXME: more documentation needed
|
||||
|
||||
Examples:
|
||||
|
||||
Create new table, setup structure, add records:
|
||||
|
||||
dbf = Dbf(filename, new=True)
|
||||
dbf.addField(
|
||||
("NAME", "C", 15),
|
||||
("SURNAME", "C", 25),
|
||||
("INITIALS", "C", 10),
|
||||
("BIRTHDATE", "D"),
|
||||
)
|
||||
for (n, s, i, b) in (
|
||||
("John", "Miller", "YC", (1980, 10, 11)),
|
||||
("Andy", "Larkin", "", (1980, 4, 11)),
|
||||
):
|
||||
rec = dbf.newRecord()
|
||||
rec["NAME"] = n
|
||||
rec["SURNAME"] = s
|
||||
rec["INITIALS"] = i
|
||||
rec["BIRTHDATE"] = b
|
||||
rec.store()
|
||||
dbf.close()
|
||||
|
||||
Open existed dbf, read some data:
|
||||
|
||||
dbf = Dbf(filename, True)
|
||||
for rec in dbf:
|
||||
for fldName in dbf.fieldNames:
|
||||
print '%s:\t %s (%s)' % (fldName, rec[fldName],
|
||||
type(rec[fldName]))
|
||||
print
|
||||
dbf.close()
|
||||
|
||||
"""
|
||||
"""History (most recent first):
|
||||
11-feb-2007 [als] export INVALID_VALUE;
|
||||
Dbf: added .ignoreErrors, .INVALID_VALUE
|
||||
04-jul-2006 [als] added export declaration
|
||||
20-dec-2005 [yc] removed fromStream and newDbf methods:
|
||||
use argument of __init__ call must be used instead;
|
||||
added class fields pointing to the header and
|
||||
record classes.
|
||||
17-dec-2005 [yc] split to several modules; reimplemented
|
||||
13-dec-2005 [yc] adapted to the changes of the `strutil` module.
|
||||
13-sep-2002 [als] support FoxPro Timestamp datatype
|
||||
15-nov-1999 [jjk] documentation updates, add demo
|
||||
24-aug-1998 [jjk] add some encodeValue methods (not tested), other tweaks
|
||||
08-jun-1998 [jjk] fix problems, add more features
|
||||
20-feb-1998 [jjk] fix problems, add more features
|
||||
19-feb-1998 [jjk] add create/write capabilities
|
||||
18-feb-1998 [jjk] from dbfload.py
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.7 $"[11:-2]
|
||||
__date__ = "$Date: 2007/02/11 09:23:13 $"[7:-2]
|
||||
__author__ = "Jeff Kunce <kuncej@mail.conservation.state.mo.us>"
|
||||
|
||||
__all__ = ["Dbf"]
|
||||
|
||||
from . import header
|
||||
from . import record
|
||||
from utils import INVALID_VALUE
|
||||
|
||||
|
||||
class Dbf(object):
|
||||
"""DBF accessor.
|
||||
|
||||
FIXME:
|
||||
docs and examples needed (dont' forget to tell
|
||||
about problems adding new fields on the fly)
|
||||
|
||||
Implementation notes:
|
||||
``_new`` field is used to indicate whether this is
|
||||
a new data table. `addField` could be used only for
|
||||
the new tables! If at least one record was appended
|
||||
to the table it's structure couldn't be changed.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "header", "stream",
|
||||
"_changed", "_new", "_ignore_errors")
|
||||
|
||||
HeaderClass = header.DbfHeader
|
||||
RecordClass = record.DbfRecord
|
||||
INVALID_VALUE = INVALID_VALUE
|
||||
|
||||
# initialization and creation helpers
|
||||
|
||||
def __init__(self, f, readOnly=False, new=False, ignoreErrors=False):
|
||||
"""Initialize instance.
|
||||
|
||||
Arguments:
|
||||
f:
|
||||
Filename or file-like object.
|
||||
new:
|
||||
True if new data table must be created. Assume
|
||||
data table exists if this argument is False.
|
||||
readOnly:
|
||||
if ``f`` argument is a string file will
|
||||
be opend in read-only mode; in other cases
|
||||
this argument is ignored. This argument is ignored
|
||||
even if ``new`` argument is True.
|
||||
headerObj:
|
||||
`header.DbfHeader` instance or None. If this argument
|
||||
is None, new empty header will be used with the
|
||||
all fields set by default.
|
||||
ignoreErrors:
|
||||
if set, failing field value conversion will return
|
||||
``INVALID_VALUE`` instead of raising conversion error.
|
||||
|
||||
"""
|
||||
if isinstance(f, basestring):
|
||||
# a filename
|
||||
self.name = f
|
||||
if new:
|
||||
# new table (table file must be
|
||||
# created or opened and truncated)
|
||||
self.stream = file(f, "w+b")
|
||||
else:
|
||||
# tabe file must exist
|
||||
self.stream = file(f, ("r+b", "rb")[bool(readOnly)])
|
||||
else:
|
||||
# a stream
|
||||
self.name = getattr(f, "name", "")
|
||||
self.stream = f
|
||||
if new:
|
||||
# if this is a new table, header will be empty
|
||||
self.header = self.HeaderClass()
|
||||
else:
|
||||
# or instantiated using stream
|
||||
self.header = self.HeaderClass.fromStream(self.stream)
|
||||
self.ignoreErrors = ignoreErrors
|
||||
self._new = bool(new)
|
||||
self._changed = False
|
||||
|
||||
# properties
|
||||
|
||||
closed = property(lambda self: self.stream.closed)
|
||||
recordCount = property(lambda self: self.header.recordCount)
|
||||
fieldNames = property(
|
||||
lambda self: [_fld.name for _fld in self.header.fields])
|
||||
fieldDefs = property(lambda self: self.header.fields)
|
||||
changed = property(lambda self: self._changed or self.header.changed)
|
||||
|
||||
def ignoreErrors(self, value):
|
||||
"""Update `ignoreErrors` flag on the header object and self"""
|
||||
self.header.ignoreErrors = self._ignore_errors = bool(value)
|
||||
|
||||
ignoreErrors = property(
|
||||
lambda self: self._ignore_errors,
|
||||
ignoreErrors,
|
||||
doc="""Error processing mode for DBF field value conversion
|
||||
|
||||
if set, failing field value conversion will return
|
||||
``INVALID_VALUE`` instead of raising conversion error.
|
||||
|
||||
""")
|
||||
|
||||
# protected methods
|
||||
|
||||
def _fixIndex(self, index):
|
||||
"""Return fixed index.
|
||||
|
||||
This method fails if index isn't a numeric object
|
||||
(long or int). Or index isn't in a valid range
|
||||
(less or equal to the number of records in the db).
|
||||
|
||||
If ``index`` is a negative number, it will be
|
||||
treated as a negative indexes for list objects.
|
||||
|
||||
Return:
|
||||
Return value is numeric object maning valid index.
|
||||
|
||||
"""
|
||||
if not isinstance(index, (int, long)):
|
||||
raise TypeError("Index must be a numeric object")
|
||||
if index < 0:
|
||||
# index from the right side
|
||||
# fix it to the left-side index
|
||||
index += len(self) + 1
|
||||
if index >= len(self):
|
||||
raise IndexError("Record index out of range")
|
||||
return index
|
||||
|
||||
# iterface methods
|
||||
|
||||
def close(self):
|
||||
self.flush()
|
||||
self.stream.close()
|
||||
|
||||
def flush(self):
|
||||
"""Flush data to the associated stream."""
|
||||
if self.changed:
|
||||
self.header.setCurrentDate()
|
||||
self.header.write(self.stream)
|
||||
self.stream.flush()
|
||||
self._changed = False
|
||||
|
||||
def indexOfFieldName(self, name):
|
||||
"""Index of field named ``name``."""
|
||||
# FIXME: move this to header class
|
||||
return self.header.fields.index(name)
|
||||
|
||||
def newRecord(self):
|
||||
"""Return new record, which belong to this table."""
|
||||
return self.RecordClass(self)
|
||||
|
||||
def append(self, record):
|
||||
"""Append ``record`` to the database."""
|
||||
record.index = self.header.recordCount
|
||||
record._write()
|
||||
self.header.recordCount += 1
|
||||
self._changed = True
|
||||
self._new = False
|
||||
|
||||
def addField(self, *defs):
|
||||
"""Add field definitions.
|
||||
|
||||
For more information see `header.DbfHeader.addField`.
|
||||
|
||||
"""
|
||||
if self._new:
|
||||
self.header.addField(*defs)
|
||||
else:
|
||||
raise TypeError("At least one record was added, "
|
||||
"structure can't be changed")
|
||||
|
||||
# 'magic' methods (representation and sequence interface)
|
||||
|
||||
def __repr__(self):
|
||||
return "Dbf stream '%s'\n" % self.stream + repr(self.header)
|
||||
|
||||
def __len__(self):
|
||||
"""Return number of records."""
|
||||
return self.recordCount
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Return `DbfRecord` instance."""
|
||||
return self.RecordClass.fromStream(self, self._fixIndex(index))
|
||||
|
||||
def __setitem__(self, index, record):
|
||||
"""Write `DbfRecord` instance to the stream."""
|
||||
record.index = self._fixIndex(index)
|
||||
record._write()
|
||||
self._changed = True
|
||||
self._new = False
|
||||
|
||||
# def __del__(self):
|
||||
# """Flush stream upon deletion of the object."""
|
||||
# self.flush()
|
||||
|
||||
|
||||
def demo_read(filename):
|
||||
_dbf = Dbf(filename, True)
|
||||
for _rec in _dbf:
|
||||
print
|
||||
print(repr(_rec))
|
||||
_dbf.close()
|
||||
|
||||
|
||||
def demo_create(filename):
|
||||
_dbf = Dbf(filename, new=True)
|
||||
_dbf.addField(
|
||||
("NAME", "C", 15),
|
||||
("SURNAME", "C", 25),
|
||||
("INITIALS", "C", 10),
|
||||
("BIRTHDATE", "D"),
|
||||
)
|
||||
for (_n, _s, _i, _b) in (
|
||||
("John", "Miller", "YC", (1981, 1, 2)),
|
||||
("Andy", "Larkin", "AL", (1982, 3, 4)),
|
||||
("Bill", "Clinth", "", (1983, 5, 6)),
|
||||
("Bobb", "McNail", "", (1984, 7, 8)),
|
||||
):
|
||||
_rec = _dbf.newRecord()
|
||||
_rec["NAME"] = _n
|
||||
_rec["SURNAME"] = _s
|
||||
_rec["INITIALS"] = _i
|
||||
_rec["BIRTHDATE"] = _b
|
||||
_rec.store()
|
||||
print(repr(_dbf))
|
||||
_dbf.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
_name = len(sys.argv) > 1 and sys.argv[1] or "county.dbf"
|
||||
demo_create(_name)
|
||||
demo_read(_name)
|
||||
|
||||
# vim: set et sw=4 sts=4 :
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/python
|
||||
""".DBF creation helpers.
|
||||
|
||||
Note: this is a legacy interface. New code should use Dbf class
|
||||
for table creation (see examples in dbf.py)
|
||||
|
||||
TODO:
|
||||
- handle Memo fields.
|
||||
- check length of the fields accoring to the
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
|
||||
"""
|
||||
"""History (most recent first)
|
||||
04-jul-2006 [als] added export declaration;
|
||||
updated for dbfpy 2.0
|
||||
15-dec-2005 [yc] define dbf_new.__slots__
|
||||
14-dec-2005 [yc] added vim modeline; retab'd; added doc-strings;
|
||||
dbf_new now is a new class (inherited from object)
|
||||
??-jun-2000 [--] added by Hans Fiby
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.4 $"[11:-2]
|
||||
__date__ = "$Date: 2006/07/04 08:18:18 $"[7:-2]
|
||||
|
||||
__all__ = ["dbf_new"]
|
||||
|
||||
from dbf import *
|
||||
from fields import *
|
||||
from header import *
|
||||
from record import *
|
||||
|
||||
|
||||
class _FieldDefinition(object):
|
||||
"""Field definition.
|
||||
|
||||
This is a simple structure, which contains ``name``, ``type``,
|
||||
``len``, ``dec`` and ``cls`` fields.
|
||||
|
||||
Objects also implement get/setitem magic functions, so fields
|
||||
could be accessed via sequence iterface, where 'name' has
|
||||
index 0, 'type' index 1, 'len' index 2, 'dec' index 3 and
|
||||
'cls' could be located at index 4.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = "name", "type", "len", "dec", "cls"
|
||||
|
||||
# WARNING: be attentive - dictionaries are mutable!
|
||||
FLD_TYPES = {
|
||||
# type: (cls, len)
|
||||
"C": (DbfCharacterFieldDef, None),
|
||||
"N": (DbfNumericFieldDef, None),
|
||||
"L": (DbfLogicalFieldDef, 1),
|
||||
# FIXME: support memos
|
||||
# "M": (DbfMemoFieldDef),
|
||||
"D": (DbfDateFieldDef, 8),
|
||||
# FIXME: I'm not sure length should be 14 characters!
|
||||
# but temporary I use it, cuz date is 8 characters
|
||||
# and time 6 (hhmmss)
|
||||
"T": (DbfDateTimeFieldDef, 14),
|
||||
}
|
||||
|
||||
def __init__(self, name, type, len=None, dec=0):
|
||||
_cls, _len = self.FLD_TYPES[type]
|
||||
if _len is None:
|
||||
if len is None:
|
||||
raise ValueError("Field length must be defined")
|
||||
_len = len
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.len = _len
|
||||
self.dec = dec
|
||||
self.cls = _cls
|
||||
|
||||
def getDbfField(self):
|
||||
"Return `DbfFieldDef` instance from the current definition."
|
||||
return self.cls(self.name, self.len, self.dec)
|
||||
|
||||
def appendToHeader(self, dbfh):
|
||||
"""Create a `DbfFieldDef` instance and append it to the dbf header.
|
||||
|
||||
Arguments:
|
||||
dbfh: `DbfHeader` instance.
|
||||
|
||||
"""
|
||||
_dbff = self.getDbfField()
|
||||
dbfh.addField(_dbff)
|
||||
|
||||
|
||||
class dbf_new(object):
|
||||
"""New .DBF creation helper.
|
||||
|
||||
Example Usage:
|
||||
|
||||
dbfn = dbf_new()
|
||||
dbfn.add_field("name",'C',80)
|
||||
dbfn.add_field("price",'N',10,2)
|
||||
dbfn.add_field("date",'D',8)
|
||||
dbfn.write("tst.dbf")
|
||||
|
||||
Note:
|
||||
This module cannot handle Memo-fields,
|
||||
they are special.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("fields",)
|
||||
|
||||
FieldDefinitionClass = _FieldDefinition
|
||||
|
||||
def __init__(self):
|
||||
self.fields = []
|
||||
|
||||
def add_field(self, name, typ, len, dec=0):
|
||||
"""Add field definition.
|
||||
|
||||
Arguments:
|
||||
name:
|
||||
field name (str object). field name must not
|
||||
contain ASCII NULs and it's length shouldn't
|
||||
exceed 10 characters.
|
||||
typ:
|
||||
type of the field. this must be a single character
|
||||
from the "CNLMDT" set meaning character, numeric,
|
||||
logical, memo, date and date/time respectively.
|
||||
len:
|
||||
length of the field. this argument is used only for
|
||||
the character and numeric fields. all other fields
|
||||
have fixed length.
|
||||
FIXME: use None as a default for this argument?
|
||||
dec:
|
||||
decimal precision. used only for the numric fields.
|
||||
|
||||
"""
|
||||
self.fields.append(self.FieldDefinitionClass(name, typ, len, dec))
|
||||
|
||||
def write(self, filename):
|
||||
"""Create empty .DBF file using current structure."""
|
||||
_dbfh = DbfHeader()
|
||||
_dbfh.setCurrentDate()
|
||||
for _fldDef in self.fields:
|
||||
_fldDef.appendToHeader(_dbfh)
|
||||
_dbfStream = file(filename, "wb")
|
||||
_dbfh.write(_dbfStream)
|
||||
_dbfStream.close()
|
||||
|
||||
def write_stream(self, stream):
|
||||
_dbfh = DbfHeader()
|
||||
_dbfh.setCurrentDate()
|
||||
for _fldDef in self.fields:
|
||||
_fldDef.appendToHeader(_dbfh)
|
||||
_dbfh.write(stream)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# create a new DBF-File
|
||||
dbfn = dbf_new()
|
||||
dbfn.add_field("name", 'C', 80)
|
||||
dbfn.add_field("price", 'N', 10, 2)
|
||||
dbfn.add_field("date", 'D', 8)
|
||||
dbfn.write("tst.dbf")
|
||||
# test new dbf
|
||||
print "*** created tst.dbf: ***"
|
||||
dbft = Dbf('tst.dbf', readOnly=0)
|
||||
print repr(dbft)
|
||||
# add a record
|
||||
rec = DbfRecord(dbft)
|
||||
rec['name'] = 'something'
|
||||
rec['price'] = 10.5
|
||||
rec['date'] = (2000, 1, 12)
|
||||
rec.store()
|
||||
# add another record
|
||||
rec = DbfRecord(dbft)
|
||||
rec['name'] = 'foo and bar'
|
||||
rec['price'] = 12234
|
||||
rec['date'] = (1992, 7, 15)
|
||||
rec.store()
|
||||
|
||||
# show the records
|
||||
print "*** inserted 2 records into tst.dbf: ***"
|
||||
print repr(dbft)
|
||||
for i1 in range(len(dbft)):
|
||||
rec = dbft[i1]
|
||||
for fldName in dbft.fieldNames:
|
||||
print '%s:\t %s' % (fldName, rec[fldName])
|
||||
print
|
||||
dbft.close()
|
||||
|
||||
# vim: set et sts=4 sw=4 :
|
||||
@@ -0,0 +1,466 @@
|
||||
"""DBF fields definitions.
|
||||
|
||||
TODO:
|
||||
- make memos work
|
||||
"""
|
||||
"""History (most recent first):
|
||||
26-may-2009 [als] DbfNumericFieldDef.decodeValue: strip zero bytes
|
||||
05-feb-2009 [als] DbfDateFieldDef.encodeValue: empty arg produces empty date
|
||||
16-sep-2008 [als] DbfNumericFieldDef decoding looks for decimal point
|
||||
in the value to select float or integer return type
|
||||
13-mar-2008 [als] check field name length in constructor
|
||||
11-feb-2007 [als] handle value conversion errors
|
||||
10-feb-2007 [als] DbfFieldDef: added .rawFromRecord()
|
||||
01-dec-2006 [als] Timestamp columns use None for empty values
|
||||
31-oct-2006 [als] support field types 'F' (float), 'I' (integer)
|
||||
and 'Y' (currency);
|
||||
automate export and registration of field classes
|
||||
04-jul-2006 [als] added export declaration
|
||||
10-mar-2006 [als] decode empty values for Date and Logical fields;
|
||||
show field name in errors
|
||||
10-mar-2006 [als] fix Numeric value decoding: according to spec,
|
||||
value always is string representation of the number;
|
||||
ensure that encoded Numeric value fits into the field
|
||||
20-dec-2005 [yc] use field names in upper case
|
||||
15-dec-2005 [yc] field definitions moved from `dbf`.
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.14 $"[11:-2]
|
||||
__date__ = "$Date: 2009/05/26 05:16:51 $"[7:-2]
|
||||
|
||||
__all__ = ["lookupFor",] # field classes added at the end of the module
|
||||
|
||||
import datetime
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from . import utils
|
||||
|
||||
## abstract definitions
|
||||
|
||||
class DbfFieldDef(object):
|
||||
"""Abstract field definition.
|
||||
|
||||
Child classes must override ``type`` class attribute to provide datatype
|
||||
infromation of the field definition. For more info about types visit
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
|
||||
Also child classes must override ``defaultValue`` field to provide
|
||||
default value for the field value.
|
||||
|
||||
If child class has fixed length ``length`` class attribute must be
|
||||
overriden and set to the valid value. None value means, that field
|
||||
isn't of fixed length.
|
||||
|
||||
Note: ``name`` field must not be changed after instantiation.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "length", "decimalCount",
|
||||
"start", "end", "ignoreErrors")
|
||||
|
||||
# length of the field, None in case of variable-length field,
|
||||
# or a number if this field is a fixed-length field
|
||||
length = None
|
||||
|
||||
# field type. for more information about fields types visit
|
||||
# `http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
# must be overriden in child classes
|
||||
typeCode = None
|
||||
|
||||
# default value for the field. this field must be
|
||||
# overriden in child classes
|
||||
defaultValue = None
|
||||
|
||||
def __init__(self, name, length=None, decimalCount=None,
|
||||
start=None, stop=None, ignoreErrors=False,
|
||||
):
|
||||
"""Initialize instance."""
|
||||
assert self.typeCode is not None, "Type code must be overriden"
|
||||
assert self.defaultValue is not None, "Default value must be overriden"
|
||||
## fix arguments
|
||||
if len(name) >10:
|
||||
raise ValueError("Field name \"%s\" is too long" % name)
|
||||
name = str(name).upper()
|
||||
if self.__class__.length is None:
|
||||
if length is None:
|
||||
raise ValueError("[%s] Length isn't specified" % name)
|
||||
length = int(length)
|
||||
if length <= 0:
|
||||
raise ValueError("[%s] Length must be a positive integer"
|
||||
% name)
|
||||
else:
|
||||
length = self.length
|
||||
if decimalCount is None:
|
||||
decimalCount = 0
|
||||
## set fields
|
||||
self.name = name
|
||||
# FIXME: validate length according to the specification at
|
||||
# http://www.clicketyclick.dk/databases/xbase/format/data_types.html
|
||||
self.length = length
|
||||
self.decimalCount = decimalCount
|
||||
self.ignoreErrors = ignoreErrors
|
||||
self.start = start
|
||||
self.end = stop
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.name, str(other).upper())
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def fromString(cls, string, start, ignoreErrors=False):
|
||||
"""Decode dbf field definition from the string data.
|
||||
|
||||
Arguments:
|
||||
string:
|
||||
a string, dbf definition is decoded from. length of
|
||||
the string must be 32 bytes.
|
||||
start:
|
||||
position in the database file.
|
||||
ignoreErrors:
|
||||
initial error processing mode for the new field (boolean)
|
||||
|
||||
"""
|
||||
assert len(string) == 32
|
||||
_length = ord(string[16])
|
||||
return cls(utils.unzfill(string)[:11], _length, ord(string[17]),
|
||||
start, start + _length, ignoreErrors=ignoreErrors)
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
def toString(self):
|
||||
"""Return encoded field definition.
|
||||
|
||||
Return:
|
||||
Return value is a string object containing encoded
|
||||
definition of this field.
|
||||
|
||||
"""
|
||||
if sys.version_info < (2, 4):
|
||||
# earlier versions did not support padding character
|
||||
_name = self.name[:11] + "\0" * (11 - len(self.name))
|
||||
else:
|
||||
_name = self.name.ljust(11, '\0')
|
||||
return (
|
||||
_name +
|
||||
self.typeCode +
|
||||
#data address
|
||||
chr(0) * 4 +
|
||||
chr(self.length) +
|
||||
chr(self.decimalCount) +
|
||||
chr(0) * 14
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "%-10s %1s %3d %3d" % self.fieldInfo()
|
||||
|
||||
def fieldInfo(self):
|
||||
"""Return field information.
|
||||
|
||||
Return:
|
||||
Return value is a (name, type, length, decimals) tuple.
|
||||
|
||||
"""
|
||||
return (self.name, self.typeCode, self.length, self.decimalCount)
|
||||
|
||||
def rawFromRecord(self, record):
|
||||
"""Return a "raw" field value from the record string."""
|
||||
return record[self.start:self.end]
|
||||
|
||||
def decodeFromRecord(self, record):
|
||||
"""Return decoded field value from the record string."""
|
||||
try:
|
||||
return self.decodeValue(self.rawFromRecord(record))
|
||||
except:
|
||||
if self.ignoreErrors:
|
||||
return utils.INVALID_VALUE
|
||||
else:
|
||||
raise
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return decoded value from string value.
|
||||
|
||||
This method shouldn't be used publicly. It's called from the
|
||||
`decodeFromRecord` method.
|
||||
|
||||
This is an abstract method and it must be overridden in child classes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return str object containing encoded field value.
|
||||
|
||||
This is an abstract method and it must be overriden in child classes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
## real classes
|
||||
|
||||
class DbfCharacterFieldDef(DbfFieldDef):
|
||||
"""Definition of the character field."""
|
||||
|
||||
typeCode = "C"
|
||||
defaultValue = ""
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return string object.
|
||||
|
||||
Return value is a ``value`` argument with stripped right spaces.
|
||||
|
||||
"""
|
||||
return value.rstrip(" ")
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return raw data string encoded from a ``value``."""
|
||||
return str(value)[:self.length].ljust(self.length)
|
||||
|
||||
|
||||
class DbfNumericFieldDef(DbfFieldDef):
|
||||
"""Definition of the numeric field."""
|
||||
|
||||
typeCode = "N"
|
||||
# XXX: now I'm not sure it was a good idea to make a class field
|
||||
# `defaultValue` instead of a generic method as it was implemented
|
||||
# previously -- it's ok with all types except number, cuz
|
||||
# if self.decimalCount is 0, we should return 0 and 0.0 otherwise.
|
||||
defaultValue = 0
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return a number decoded from ``value``.
|
||||
|
||||
If decimals is zero, value will be decoded as an integer;
|
||||
or as a float otherwise.
|
||||
|
||||
Return:
|
||||
Return value is a int (long) or float instance.
|
||||
|
||||
"""
|
||||
value = value.strip(" \0")
|
||||
if "." in value:
|
||||
# a float (has decimal separator)
|
||||
return float(value)
|
||||
elif value:
|
||||
# must be an integer
|
||||
return int(value)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return string containing encoded ``value``."""
|
||||
_rv = ("%*.*f" % (self.length, self.decimalCount, value))
|
||||
if len(_rv) > self.length:
|
||||
_ppos = _rv.find(".")
|
||||
if 0 <= _ppos <= self.length:
|
||||
_rv = _rv[:self.length]
|
||||
else:
|
||||
raise ValueError("[%s] Numeric overflow: %s (field width: %i)"
|
||||
% (self.name, _rv, self.length))
|
||||
return _rv
|
||||
|
||||
class DbfFloatFieldDef(DbfNumericFieldDef):
|
||||
"""Definition of the float field - same as numeric."""
|
||||
|
||||
typeCode = "F"
|
||||
|
||||
class DbfIntegerFieldDef(DbfFieldDef):
|
||||
"""Definition of the integer field."""
|
||||
|
||||
typeCode = "I"
|
||||
length = 4
|
||||
defaultValue = 0
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return an integer number decoded from ``value``."""
|
||||
return struct.unpack("<i", value)[0]
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return string containing encoded ``value``."""
|
||||
return struct.pack("<i", int(value))
|
||||
|
||||
class DbfCurrencyFieldDef(DbfFieldDef):
|
||||
"""Definition of the currency field."""
|
||||
|
||||
typeCode = "Y"
|
||||
length = 8
|
||||
defaultValue = 0.0
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return float number decoded from ``value``."""
|
||||
return struct.unpack("<q", value)[0] / 10000.
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return string containing encoded ``value``."""
|
||||
return struct.pack("<q", round(value * 10000))
|
||||
|
||||
class DbfLogicalFieldDef(DbfFieldDef):
|
||||
"""Definition of the logical field."""
|
||||
|
||||
typeCode = "L"
|
||||
defaultValue = -1
|
||||
length = 1
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return True, False or -1 decoded from ``value``."""
|
||||
# Note: value always is 1-char string
|
||||
if value == "?":
|
||||
return -1
|
||||
if value in "NnFf ":
|
||||
return False
|
||||
if value in "YyTt":
|
||||
return True
|
||||
raise ValueError("[%s] Invalid logical value %r" % (self.name, value))
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return a character from the "TF?" set.
|
||||
|
||||
Return:
|
||||
Return value is "T" if ``value`` is True
|
||||
"?" if value is -1 or False otherwise.
|
||||
|
||||
"""
|
||||
if value is True:
|
||||
return "T"
|
||||
if value == -1:
|
||||
return "?"
|
||||
return "F"
|
||||
|
||||
|
||||
class DbfMemoFieldDef(DbfFieldDef):
|
||||
"""Definition of the memo field.
|
||||
|
||||
Note: memos aren't currenly completely supported.
|
||||
|
||||
"""
|
||||
|
||||
typeCode = "M"
|
||||
defaultValue = " " * 10
|
||||
length = 10
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return int .dbt block number decoded from the string object."""
|
||||
#return int(value)
|
||||
raise NotImplementedError
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return raw data string encoded from a ``value``.
|
||||
|
||||
Note: this is an internal method.
|
||||
|
||||
"""
|
||||
#return str(value)[:self.length].ljust(self.length)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DbfDateFieldDef(DbfFieldDef):
|
||||
"""Definition of the date field."""
|
||||
|
||||
typeCode = "D"
|
||||
defaultValue = utils.classproperty(lambda cls: datetime.date.today())
|
||||
# "yyyymmdd" gives us 8 characters
|
||||
length = 8
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return a ``datetime.date`` instance decoded from ``value``."""
|
||||
if value.strip():
|
||||
return utils.getDate(value)
|
||||
else:
|
||||
return None
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return a string-encoded value.
|
||||
|
||||
``value`` argument should be a value suitable for the
|
||||
`utils.getDate` call.
|
||||
|
||||
Return:
|
||||
Return value is a string in format "yyyymmdd".
|
||||
|
||||
"""
|
||||
if value:
|
||||
return utils.getDate(value).strftime("%Y%m%d")
|
||||
else:
|
||||
return " " * self.length
|
||||
|
||||
|
||||
class DbfDateTimeFieldDef(DbfFieldDef):
|
||||
"""Definition of the timestamp field."""
|
||||
|
||||
# a difference between JDN (Julian Day Number)
|
||||
# and GDN (Gregorian Day Number). note, that GDN < JDN
|
||||
JDN_GDN_DIFF = 1721425
|
||||
typeCode = "T"
|
||||
defaultValue = utils.classproperty(lambda cls: datetime.datetime.now())
|
||||
# two 32-bits integers representing JDN and amount of
|
||||
# milliseconds respectively gives us 8 bytes.
|
||||
# note, that values must be encoded in LE byteorder.
|
||||
length = 8
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return a `datetime.datetime` instance."""
|
||||
assert len(value) == self.length
|
||||
# LE byteorder
|
||||
_jdn, _msecs = struct.unpack("<2I", value)
|
||||
if _jdn >= 1:
|
||||
_rv = datetime.datetime.fromordinal(_jdn - self.JDN_GDN_DIFF)
|
||||
_rv += datetime.timedelta(0, _msecs / 1000.0)
|
||||
else:
|
||||
# empty date
|
||||
_rv = None
|
||||
return _rv
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return a string-encoded ``value``."""
|
||||
if value:
|
||||
value = utils.getDateTime(value)
|
||||
# LE byteorder
|
||||
_rv = struct.pack("<2I", value.toordinal() + self.JDN_GDN_DIFF,
|
||||
(value.hour * 3600 + value.minute * 60 + value.second) * 1000)
|
||||
else:
|
||||
_rv = "\0" * self.length
|
||||
assert len(_rv) == self.length
|
||||
return _rv
|
||||
|
||||
|
||||
_fieldsRegistry = {}
|
||||
|
||||
def registerField(fieldCls):
|
||||
"""Register field definition class.
|
||||
|
||||
``fieldCls`` should be subclass of the `DbfFieldDef`.
|
||||
|
||||
Use `lookupFor` to retrieve field definition class
|
||||
by the type code.
|
||||
|
||||
"""
|
||||
assert fieldCls.typeCode is not None, "Type code isn't defined"
|
||||
# XXX: use fieldCls.typeCode.upper()? in case of any decign
|
||||
# don't forget to look to the same comment in ``lookupFor`` method
|
||||
_fieldsRegistry[fieldCls.typeCode] = fieldCls
|
||||
|
||||
|
||||
def lookupFor(typeCode):
|
||||
"""Return field definition class for the given type code.
|
||||
|
||||
``typeCode`` must be a single character. That type should be
|
||||
previously registered.
|
||||
|
||||
Use `registerField` to register new field class.
|
||||
|
||||
Return:
|
||||
Return value is a subclass of the `DbfFieldDef`.
|
||||
|
||||
"""
|
||||
# XXX: use typeCode.upper()? in case of any decign don't
|
||||
# forget to look to the same comment in ``registerField``
|
||||
return _fieldsRegistry[typeCode]
|
||||
|
||||
## register generic types
|
||||
|
||||
for (_name, _val) in globals().items():
|
||||
if isinstance(_val, type) and issubclass(_val, DbfFieldDef) \
|
||||
and (_name != "DbfFieldDef"):
|
||||
__all__.append(_name)
|
||||
registerField(_val)
|
||||
del _name, _val
|
||||
|
||||
# vim: et sts=4 sw=4 :
|
||||
@@ -0,0 +1,275 @@
|
||||
"""DBF header definition.
|
||||
|
||||
TODO:
|
||||
- handle encoding of the character fields
|
||||
(encoding information stored in the DBF header)
|
||||
|
||||
"""
|
||||
"""History (most recent first):
|
||||
16-sep-2010 [als] fromStream: fix century of the last update field
|
||||
11-feb-2007 [als] added .ignoreErrors
|
||||
10-feb-2007 [als] added __getitem__: return field definitions
|
||||
by field name or field number (zero-based)
|
||||
04-jul-2006 [als] added export declaration
|
||||
15-dec-2005 [yc] created
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.6 $"[11:-2]
|
||||
__date__ = "$Date: 2010/09/16 05:06:39 $"[7:-2]
|
||||
|
||||
__all__ = ["DbfHeader"]
|
||||
|
||||
try:
|
||||
import cStringIO
|
||||
except ImportError:
|
||||
# when we're in python3, we cStringIO has been replaced by io.StringIO
|
||||
import io as cStringIO
|
||||
import datetime
|
||||
import struct
|
||||
import time
|
||||
|
||||
from . import fields
|
||||
from . import utils
|
||||
|
||||
|
||||
class DbfHeader(object):
|
||||
"""Dbf header definition.
|
||||
|
||||
For more information about dbf header format visit
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_STRUCT`
|
||||
|
||||
Examples:
|
||||
Create an empty dbf header and add some field definitions:
|
||||
dbfh = DbfHeader()
|
||||
dbfh.addField(("name", "C", 10))
|
||||
dbfh.addField(("date", "D"))
|
||||
dbfh.addField(DbfNumericFieldDef("price", 5, 2))
|
||||
Create a dbf header with field definitions:
|
||||
dbfh = DbfHeader([
|
||||
("name", "C", 10),
|
||||
("date", "D"),
|
||||
DbfNumericFieldDef("price", 5, 2),
|
||||
])
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("signature", "fields", "lastUpdate", "recordLength",
|
||||
"recordCount", "headerLength", "changed", "_ignore_errors")
|
||||
|
||||
## instance construction and initialization methods
|
||||
|
||||
def __init__(self, fields=None, headerLength=0, recordLength=0,
|
||||
recordCount=0, signature=0x03, lastUpdate=None, ignoreErrors=False,
|
||||
):
|
||||
"""Initialize instance.
|
||||
|
||||
Arguments:
|
||||
fields:
|
||||
a list of field definitions;
|
||||
recordLength:
|
||||
size of the records;
|
||||
headerLength:
|
||||
size of the header;
|
||||
recordCount:
|
||||
number of records stored in DBF;
|
||||
signature:
|
||||
version number (aka signature). using 0x03 as a default meaning
|
||||
"File without DBT". for more information about this field visit
|
||||
``http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_NOTE_1_TARGET``
|
||||
lastUpdate:
|
||||
date of the DBF's update. this could be a string ('yymmdd' or
|
||||
'yyyymmdd'), timestamp (int or float), datetime/date value,
|
||||
a sequence (assuming (yyyy, mm, dd, ...)) or an object having
|
||||
callable ``ticks`` field.
|
||||
ignoreErrors:
|
||||
error processing mode for DBF fields (boolean)
|
||||
|
||||
"""
|
||||
self.signature = signature
|
||||
if fields is None:
|
||||
self.fields = []
|
||||
else:
|
||||
self.fields = list(fields)
|
||||
self.lastUpdate = utils.getDate(lastUpdate)
|
||||
self.recordLength = recordLength
|
||||
self.headerLength = headerLength
|
||||
self.recordCount = recordCount
|
||||
self.ignoreErrors = ignoreErrors
|
||||
# XXX: I'm not sure this is safe to
|
||||
# initialize `self.changed` in this way
|
||||
self.changed = bool(self.fields)
|
||||
|
||||
# @classmethod
|
||||
def fromString(cls, string):
|
||||
"""Return header instance from the string object."""
|
||||
return cls.fromStream(cStringIO.StringIO(str(string)))
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
# @classmethod
|
||||
def fromStream(cls, stream):
|
||||
"""Return header object from the stream."""
|
||||
stream.seek(0)
|
||||
_data = stream.read(32)
|
||||
(_cnt, _hdrLen, _recLen) = struct.unpack("<I2H", _data[4:12])
|
||||
#reserved = _data[12:32]
|
||||
_year = ord(_data[1])
|
||||
if _year < 80:
|
||||
# dBase II started at 1980. It is quite unlikely
|
||||
# that actual last update date is before that year.
|
||||
_year += 2000
|
||||
else:
|
||||
_year += 1900
|
||||
## create header object
|
||||
_obj = cls(None, _hdrLen, _recLen, _cnt, ord(_data[0]),
|
||||
(_year, ord(_data[2]), ord(_data[3])))
|
||||
## append field definitions
|
||||
# position 0 is for the deletion flag
|
||||
_pos = 1
|
||||
_data = stream.read(1)
|
||||
|
||||
# The field definitions are ended either by \x0D OR a newline
|
||||
# character, so we need to handle both when reading from a stream.
|
||||
# When writing, dbfpy appears to write newlines instead of \x0D.
|
||||
while _data[0] not in ["\x0D", "\n"]:
|
||||
_data += stream.read(31)
|
||||
_fld = fields.lookupFor(_data[11]).fromString(_data, _pos)
|
||||
_obj._addField(_fld)
|
||||
_pos = _fld.end
|
||||
_data = stream.read(1)
|
||||
return _obj
|
||||
fromStream = classmethod(fromStream)
|
||||
|
||||
## properties
|
||||
|
||||
year = property(lambda self: self.lastUpdate.year)
|
||||
month = property(lambda self: self.lastUpdate.month)
|
||||
day = property(lambda self: self.lastUpdate.day)
|
||||
|
||||
def ignoreErrors(self, value):
|
||||
"""Update `ignoreErrors` flag on self and all fields"""
|
||||
self._ignore_errors = value = bool(value)
|
||||
for _field in self.fields:
|
||||
_field.ignoreErrors = value
|
||||
ignoreErrors = property(
|
||||
lambda self: self._ignore_errors,
|
||||
ignoreErrors,
|
||||
doc="""Error processing mode for DBF field value conversion
|
||||
|
||||
if set, failing field value conversion will return
|
||||
``INVALID_VALUE`` instead of raising conversion error.
|
||||
|
||||
""")
|
||||
|
||||
## object representation
|
||||
|
||||
def __repr__(self):
|
||||
_rv = """\
|
||||
Version (signature): 0x%02x
|
||||
Last update: %s
|
||||
Header length: %d
|
||||
Record length: %d
|
||||
Record count: %d
|
||||
FieldName Type Len Dec
|
||||
""" % (self.signature, self.lastUpdate, self.headerLength,
|
||||
self.recordLength, self.recordCount)
|
||||
_rv += "\n".join(
|
||||
["%10s %4s %3s %3s" % _fld.fieldInfo() for _fld in self.fields]
|
||||
)
|
||||
return _rv
|
||||
|
||||
## internal methods
|
||||
|
||||
def _addField(self, *defs):
|
||||
"""Internal variant of the `addField` method.
|
||||
|
||||
This method doesn't set `self.changed` field to True.
|
||||
|
||||
Return value is a length of the appended records.
|
||||
Note: this method doesn't modify ``recordLength`` and
|
||||
``headerLength`` fields. Use `addField` instead of this
|
||||
method if you don't exactly know what you're doing.
|
||||
|
||||
"""
|
||||
# insure we have dbf.DbfFieldDef instances first (instantiation
|
||||
# from the tuple could raise an error, in such a case I don't
|
||||
# wanna add any of the definitions -- all will be ignored)
|
||||
_defs = []
|
||||
_recordLength = 0
|
||||
for _def in defs:
|
||||
if isinstance(_def, fields.DbfFieldDef):
|
||||
_obj = _def
|
||||
else:
|
||||
(_name, _type, _len, _dec) = (tuple(_def) + (None,) * 4)[:4]
|
||||
_cls = fields.lookupFor(_type)
|
||||
_obj = _cls(_name, _len, _dec,
|
||||
ignoreErrors=self._ignore_errors)
|
||||
_recordLength += _obj.length
|
||||
_defs.append(_obj)
|
||||
# and now extend field definitions and
|
||||
# update record length
|
||||
self.fields += _defs
|
||||
return _recordLength
|
||||
|
||||
## interface methods
|
||||
|
||||
def addField(self, *defs):
|
||||
"""Add field definition to the header.
|
||||
|
||||
Examples:
|
||||
dbfh.addField(
|
||||
("name", "C", 20),
|
||||
dbf.DbfCharacterFieldDef("surname", 20),
|
||||
dbf.DbfDateFieldDef("birthdate"),
|
||||
("member", "L"),
|
||||
)
|
||||
dbfh.addField(("price", "N", 5, 2))
|
||||
dbfh.addField(dbf.DbfNumericFieldDef("origprice", 5, 2))
|
||||
|
||||
"""
|
||||
_oldLen = self.recordLength
|
||||
self.recordLength += self._addField(*defs)
|
||||
if not _oldLen:
|
||||
self.recordLength += 1
|
||||
# XXX: may be just use:
|
||||
# self.recordeLength += self._addField(*defs) + bool(not _oldLen)
|
||||
# recalculate headerLength
|
||||
self.headerLength = 32 + (32 * len(self.fields)) + 1
|
||||
self.changed = True
|
||||
|
||||
def write(self, stream):
|
||||
"""Encode and write header to the stream."""
|
||||
stream.seek(0)
|
||||
stream.write(self.toString())
|
||||
stream.write("".join([_fld.toString() for _fld in self.fields]))
|
||||
stream.write(chr(0x0D)) # cr at end of all hdr data
|
||||
self.changed = False
|
||||
|
||||
def toString(self):
|
||||
"""Returned 32 chars length string with encoded header."""
|
||||
return struct.pack("<4BI2H",
|
||||
self.signature,
|
||||
self.year - 1900,
|
||||
self.month,
|
||||
self.day,
|
||||
self.recordCount,
|
||||
self.headerLength,
|
||||
self.recordLength) + "\0" * 20
|
||||
|
||||
def setCurrentDate(self):
|
||||
"""Update ``self.lastUpdate`` field with current date value."""
|
||||
self.lastUpdate = datetime.date.today()
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Return a field definition by numeric index or name string"""
|
||||
if isinstance(item, basestring):
|
||||
_name = item.upper()
|
||||
for _field in self.fields:
|
||||
if _field.name == _name:
|
||||
return _field
|
||||
else:
|
||||
raise KeyError(item)
|
||||
else:
|
||||
# item must be field index
|
||||
return self.fields[item]
|
||||
|
||||
# vim: et sts=4 sw=4 :
|
||||
@@ -0,0 +1,262 @@
|
||||
"""DBF record definition.
|
||||
|
||||
"""
|
||||
"""History (most recent first):
|
||||
11-feb-2007 [als] __repr__: added special case for invalid field values
|
||||
10-feb-2007 [als] added .rawFromStream()
|
||||
30-oct-2006 [als] fix record length in .fromStream()
|
||||
04-jul-2006 [als] added export declaration
|
||||
20-dec-2005 [yc] DbfRecord.write() -> DbfRecord._write();
|
||||
added delete() method.
|
||||
16-dec-2005 [yc] record definition moved from `dbf`.
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.7 $"[11:-2]
|
||||
__date__ = "$Date: 2007/02/11 09:05:49 $"[7:-2]
|
||||
|
||||
__all__ = ["DbfRecord"]
|
||||
|
||||
from itertools import izip
|
||||
|
||||
import utils
|
||||
|
||||
class DbfRecord(object):
|
||||
"""DBF record.
|
||||
|
||||
Instances of this class shouldn't be created manualy,
|
||||
use `dbf.Dbf.newRecord` instead.
|
||||
|
||||
Class implements mapping/sequence interface, so
|
||||
fields could be accessed via their names or indexes
|
||||
(names is a preffered way to access fields).
|
||||
|
||||
Hint:
|
||||
Use `store` method to save modified record.
|
||||
|
||||
Examples:
|
||||
Add new record to the database:
|
||||
db = Dbf(filename)
|
||||
rec = db.newRecord()
|
||||
rec["FIELD1"] = value1
|
||||
rec["FIELD2"] = value2
|
||||
rec.store()
|
||||
Or the same, but modify existed
|
||||
(second in this case) record:
|
||||
db = Dbf(filename)
|
||||
rec = db[2]
|
||||
rec["FIELD1"] = value1
|
||||
rec["FIELD2"] = value2
|
||||
rec.store()
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = "dbf", "index", "deleted", "fieldData"
|
||||
|
||||
## creation and initialization
|
||||
|
||||
def __init__(self, dbf, index=None, deleted=False, data=None):
|
||||
"""Instance initialiation.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance this record belonogs to.
|
||||
index:
|
||||
An integer record index or None. If this value is
|
||||
None, record will be appended to the DBF.
|
||||
deleted:
|
||||
Boolean flag indicating whether this record
|
||||
is a deleted record.
|
||||
data:
|
||||
A sequence or None. This is a data of the fields.
|
||||
If this argument is None, default values will be used.
|
||||
|
||||
"""
|
||||
self.dbf = dbf
|
||||
# XXX: I'm not sure ``index`` is necessary
|
||||
self.index = index
|
||||
self.deleted = deleted
|
||||
if data is None:
|
||||
self.fieldData = [_fd.defaultValue for _fd in dbf.header.fields]
|
||||
else:
|
||||
self.fieldData = list(data)
|
||||
|
||||
# XXX: validate self.index before calculating position?
|
||||
position = property(lambda self: self.dbf.header.headerLength + \
|
||||
self.index * self.dbf.header.recordLength)
|
||||
|
||||
def rawFromStream(cls, dbf, index):
|
||||
"""Return raw record contents read from the stream.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance containing the record.
|
||||
index:
|
||||
Index of the record in the records' container.
|
||||
This argument can't be None in this call.
|
||||
|
||||
Return value is a string containing record data in DBF format.
|
||||
|
||||
"""
|
||||
# XXX: may be write smth assuming, that current stream
|
||||
# position is the required one? it could save some
|
||||
# time required to calculate where to seek in the file
|
||||
dbf.stream.seek(dbf.header.headerLength +
|
||||
index * dbf.header.recordLength)
|
||||
return dbf.stream.read(dbf.header.recordLength)
|
||||
rawFromStream = classmethod(rawFromStream)
|
||||
|
||||
def fromStream(cls, dbf, index):
|
||||
"""Return a record read from the stream.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance new record should belong to.
|
||||
index:
|
||||
Index of the record in the records' container.
|
||||
This argument can't be None in this call.
|
||||
|
||||
Return value is an instance of the current class.
|
||||
|
||||
"""
|
||||
return cls.fromString(dbf, cls.rawFromStream(dbf, index), index)
|
||||
fromStream = classmethod(fromStream)
|
||||
|
||||
def fromString(cls, dbf, string, index=None):
|
||||
"""Return record read from the string object.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance new record should belong to.
|
||||
string:
|
||||
A string new record should be created from.
|
||||
index:
|
||||
Index of the record in the container. If this
|
||||
argument is None, record will be appended.
|
||||
|
||||
Return value is an instance of the current class.
|
||||
|
||||
"""
|
||||
return cls(dbf, index, string[0]=="*",
|
||||
[_fd.decodeFromRecord(string) for _fd in dbf.header.fields])
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
## object representation
|
||||
|
||||
def __repr__(self):
|
||||
_template = "%%%ds: %%s (%%s)" % max([len(_fld)
|
||||
for _fld in self.dbf.fieldNames])
|
||||
_rv = []
|
||||
for _fld in self.dbf.fieldNames:
|
||||
_val = self[_fld]
|
||||
if _val is utils.INVALID_VALUE:
|
||||
_rv.append(_template %
|
||||
(_fld, "None", "value cannot be decoded"))
|
||||
else:
|
||||
_rv.append(_template % (_fld, _val, type(_val)))
|
||||
return "\n".join(_rv)
|
||||
|
||||
## protected methods
|
||||
|
||||
def _write(self):
|
||||
"""Write data to the dbf stream.
|
||||
|
||||
Note:
|
||||
This isn't a public method, it's better to
|
||||
use 'store' instead publically.
|
||||
Be design ``_write`` method should be called
|
||||
only from the `Dbf` instance.
|
||||
|
||||
|
||||
"""
|
||||
self._validateIndex(False)
|
||||
self.dbf.stream.seek(self.position)
|
||||
self.dbf.stream.write(self.toString())
|
||||
# FIXME: may be move this write somewhere else?
|
||||
# why we should check this condition for each record?
|
||||
if self.index == len(self.dbf):
|
||||
# this is the last record,
|
||||
# we should write SUB (ASCII 26)
|
||||
self.dbf.stream.write("\x1A")
|
||||
|
||||
## utility methods
|
||||
|
||||
def _validateIndex(self, allowUndefined=True, checkRange=False):
|
||||
"""Valid ``self.index`` value.
|
||||
|
||||
If ``allowUndefined`` argument is True functions does nothing
|
||||
in case of ``self.index`` pointing to None object.
|
||||
|
||||
"""
|
||||
if self.index is None:
|
||||
if not allowUndefined:
|
||||
raise ValueError("Index is undefined")
|
||||
elif self.index < 0:
|
||||
raise ValueError("Index can't be negative (%s)" % self.index)
|
||||
elif checkRange and self.index <= self.dbf.header.recordCount:
|
||||
raise ValueError("There are only %d records in the DBF" %
|
||||
self.dbf.header.recordCount)
|
||||
|
||||
## interface methods
|
||||
|
||||
def store(self):
|
||||
"""Store current record in the DBF.
|
||||
|
||||
If ``self.index`` is None, this record will be appended to the
|
||||
records of the DBF this records belongs to; or replaced otherwise.
|
||||
|
||||
"""
|
||||
self._validateIndex()
|
||||
if self.index is None:
|
||||
self.index = len(self.dbf)
|
||||
self.dbf.append(self)
|
||||
else:
|
||||
self.dbf[self.index] = self
|
||||
|
||||
def delete(self):
|
||||
"""Mark method as deleted."""
|
||||
self.deleted = True
|
||||
|
||||
def toString(self):
|
||||
"""Return string packed record values."""
|
||||
return "".join([" *"[self.deleted]] + [
|
||||
_def.encodeValue(_dat)
|
||||
for (_def, _dat) in izip(self.dbf.header.fields, self.fieldData)
|
||||
])
|
||||
|
||||
def asList(self):
|
||||
"""Return a flat list of fields.
|
||||
|
||||
Note:
|
||||
Change of the list's values won't change
|
||||
real values stored in this object.
|
||||
|
||||
"""
|
||||
return self.fieldData[:]
|
||||
|
||||
def asDict(self):
|
||||
"""Return a dictionary of fields.
|
||||
|
||||
Note:
|
||||
Change of the dicts's values won't change
|
||||
real values stored in this object.
|
||||
|
||||
"""
|
||||
return dict([_i for _i in izip(self.dbf.fieldNames, self.fieldData)])
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Return value by field name or field index."""
|
||||
if isinstance(key, (long, int)):
|
||||
# integer index of the field
|
||||
return self.fieldData[key]
|
||||
# assuming string field name
|
||||
return self.fieldData[self.dbf.indexOfFieldName(key)]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set field value by integer index of the field or string name."""
|
||||
if isinstance(key, (int, long)):
|
||||
# integer index of the field
|
||||
return self.fieldData[key]
|
||||
# assuming string field name
|
||||
self.fieldData[self.dbf.indexOfFieldName(key)] = value
|
||||
|
||||
# vim: et sts=4 sw=4 :
|
||||
@@ -0,0 +1,170 @@
|
||||
"""String utilities.
|
||||
|
||||
TODO:
|
||||
- allow strings in getDateTime routine;
|
||||
"""
|
||||
"""History (most recent first):
|
||||
11-feb-2007 [als] added INVALID_VALUE
|
||||
10-feb-2007 [als] allow date strings padded with spaces instead of zeroes
|
||||
20-dec-2005 [yc] handle long objects in getDate/getDateTime
|
||||
16-dec-2005 [yc] created from ``strutil`` module.
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.4 $"[11:-2]
|
||||
__date__ = "$Date: 2007/02/11 08:57:17 $"[7:-2]
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
|
||||
def unzfill(str):
|
||||
"""Return a string without ASCII NULs.
|
||||
|
||||
This function searchers for the first NUL (ASCII 0) occurance
|
||||
and truncates string till that position.
|
||||
|
||||
"""
|
||||
try:
|
||||
return str[:str.index('\0')]
|
||||
except ValueError:
|
||||
return str
|
||||
|
||||
|
||||
def getDate(date=None):
|
||||
"""Return `datetime.date` instance.
|
||||
|
||||
Type of the ``date`` argument could be one of the following:
|
||||
None:
|
||||
use current date value;
|
||||
datetime.date:
|
||||
this value will be returned;
|
||||
datetime.datetime:
|
||||
the result of the date.date() will be returned;
|
||||
string:
|
||||
assuming "%Y%m%d" or "%y%m%dd" format;
|
||||
number:
|
||||
assuming it's a timestamp (returned for example
|
||||
by the time.time() call;
|
||||
sequence:
|
||||
assuming (year, month, day, ...) sequence;
|
||||
|
||||
Additionaly, if ``date`` has callable ``ticks`` attribute,
|
||||
it will be used and result of the called would be treated
|
||||
as a timestamp value.
|
||||
|
||||
"""
|
||||
if date is None:
|
||||
# use current value
|
||||
return datetime.date.today()
|
||||
if isinstance(date, datetime.date):
|
||||
return date
|
||||
if isinstance(date, datetime.datetime):
|
||||
return date.date()
|
||||
if isinstance(date, (int, long, float)):
|
||||
# date is a timestamp
|
||||
return datetime.date.fromtimestamp(date)
|
||||
if isinstance(date, basestring):
|
||||
date = date.replace(" ", "0")
|
||||
if len(date) == 6:
|
||||
# yymmdd
|
||||
return datetime.date(*time.strptime(date, "%y%m%d")[:3])
|
||||
# yyyymmdd
|
||||
return datetime.date(*time.strptime(date, "%Y%m%d")[:3])
|
||||
if hasattr(date, "__getitem__"):
|
||||
# a sequence (assuming date/time tuple)
|
||||
return datetime.date(*date[:3])
|
||||
return datetime.date.fromtimestamp(date.ticks())
|
||||
|
||||
|
||||
def getDateTime(value=None):
|
||||
"""Return `datetime.datetime` instance.
|
||||
|
||||
Type of the ``value`` argument could be one of the following:
|
||||
None:
|
||||
use current date value;
|
||||
datetime.date:
|
||||
result will be converted to the `datetime.datetime` instance
|
||||
using midnight;
|
||||
datetime.datetime:
|
||||
``value`` will be returned as is;
|
||||
string:
|
||||
*** CURRENTLY NOT SUPPORTED ***;
|
||||
number:
|
||||
assuming it's a timestamp (returned for example
|
||||
by the time.time() call;
|
||||
sequence:
|
||||
assuming (year, month, day, ...) sequence;
|
||||
|
||||
Additionaly, if ``value`` has callable ``ticks`` attribute,
|
||||
it will be used and result of the called would be treated
|
||||
as a timestamp value.
|
||||
|
||||
"""
|
||||
if value is None:
|
||||
# use current value
|
||||
return datetime.datetime.today()
|
||||
if isinstance(value, datetime.datetime):
|
||||
return value
|
||||
if isinstance(value, datetime.date):
|
||||
return datetime.datetime.fromordinal(value.toordinal())
|
||||
if isinstance(value, (int, long, float)):
|
||||
# value is a timestamp
|
||||
return datetime.datetime.fromtimestamp(value)
|
||||
if isinstance(value, basestring):
|
||||
raise NotImplementedError("Strings aren't currently implemented")
|
||||
if hasattr(value, "__getitem__"):
|
||||
# a sequence (assuming date/time tuple)
|
||||
return datetime.datetime(*tuple(value)[:6])
|
||||
return datetime.datetime.fromtimestamp(value.ticks())
|
||||
|
||||
|
||||
class classproperty(property):
|
||||
"""Works in the same way as a ``property``, but for the classes."""
|
||||
|
||||
def __get__(self, obj, cls):
|
||||
return self.fget(cls)
|
||||
|
||||
|
||||
class _InvalidValue(object):
|
||||
|
||||
"""Value returned from DBF records when field validation fails
|
||||
|
||||
The value is not equal to anything except for itself
|
||||
and equal to all empty values: None, 0, empty string etc.
|
||||
In other words, invalid value is equal to None and not equal
|
||||
to None at the same time.
|
||||
|
||||
This value yields zero upon explicit conversion to a number type,
|
||||
empty string for string types, and False for boolean.
|
||||
|
||||
"""
|
||||
|
||||
def __eq__(self, other):
|
||||
return not other
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (other is self)
|
||||
|
||||
def __nonzero__(self):
|
||||
return False
|
||||
|
||||
def __int__(self):
|
||||
return 0
|
||||
__long__ = __int__
|
||||
|
||||
def __float__(self):
|
||||
return 0.0
|
||||
|
||||
def __str__(self):
|
||||
return ""
|
||||
|
||||
def __unicode__(self):
|
||||
return u""
|
||||
|
||||
def __repr__(self):
|
||||
return "<INVALID>"
|
||||
|
||||
# invalid value is a constant singleton
|
||||
INVALID_VALUE = _InvalidValue()
|
||||
|
||||
# vim: set et sts=4 sw=4 :
|
||||
@@ -0,0 +1,298 @@
|
||||
#! /usr/bin/env python
|
||||
"""DBF accessing helpers.
|
||||
|
||||
FIXME: more documentation needed
|
||||
|
||||
Examples:
|
||||
|
||||
Create new table, setup structure, add records:
|
||||
|
||||
dbf = Dbf(filename, new=True)
|
||||
dbf.addField(
|
||||
("NAME", "C", 15),
|
||||
("SURNAME", "C", 25),
|
||||
("INITIALS", "C", 10),
|
||||
("BIRTHDATE", "D"),
|
||||
)
|
||||
for (n, s, i, b) in (
|
||||
("John", "Miller", "YC", (1980, 10, 11)),
|
||||
("Andy", "Larkin", "", (1980, 4, 11)),
|
||||
):
|
||||
rec = dbf.newRecord()
|
||||
rec["NAME"] = n
|
||||
rec["SURNAME"] = s
|
||||
rec["INITIALS"] = i
|
||||
rec["BIRTHDATE"] = b
|
||||
rec.store()
|
||||
dbf.close()
|
||||
|
||||
Open existed dbf, read some data:
|
||||
|
||||
dbf = Dbf(filename, True)
|
||||
for rec in dbf:
|
||||
for fldName in dbf.fieldNames:
|
||||
print '%s:\t %s (%s)' % (fldName, rec[fldName],
|
||||
type(rec[fldName]))
|
||||
print
|
||||
dbf.close()
|
||||
|
||||
"""
|
||||
"""History (most recent first):
|
||||
11-feb-2007 [als] export INVALID_VALUE;
|
||||
Dbf: added .ignoreErrors, .INVALID_VALUE
|
||||
04-jul-2006 [als] added export declaration
|
||||
20-dec-2005 [yc] removed fromStream and newDbf methods:
|
||||
use argument of __init__ call must be used instead;
|
||||
added class fields pointing to the header and
|
||||
record classes.
|
||||
17-dec-2005 [yc] split to several modules; reimplemented
|
||||
13-dec-2005 [yc] adapted to the changes of the `strutil` module.
|
||||
13-sep-2002 [als] support FoxPro Timestamp datatype
|
||||
15-nov-1999 [jjk] documentation updates, add demo
|
||||
24-aug-1998 [jjk] add some encodeValue methods (not tested), other tweaks
|
||||
08-jun-1998 [jjk] fix problems, add more features
|
||||
20-feb-1998 [jjk] fix problems, add more features
|
||||
19-feb-1998 [jjk] add create/write capabilities
|
||||
18-feb-1998 [jjk] from dbfload.py
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.7 $"[11:-2]
|
||||
__date__ = "$Date: 2007/02/11 09:23:13 $"[7:-2]
|
||||
__author__ = "Jeff Kunce <kuncej@mail.conservation.state.mo.us>"
|
||||
|
||||
__all__ = ["Dbf"]
|
||||
|
||||
from . import header
|
||||
from . import record
|
||||
from .utils import INVALID_VALUE
|
||||
|
||||
|
||||
class Dbf(object):
|
||||
"""DBF accessor.
|
||||
|
||||
FIXME:
|
||||
docs and examples needed (dont' forget to tell
|
||||
about problems adding new fields on the fly)
|
||||
|
||||
Implementation notes:
|
||||
``_new`` field is used to indicate whether this is
|
||||
a new data table. `addField` could be used only for
|
||||
the new tables! If at least one record was appended
|
||||
to the table it's structure couldn't be changed.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "header", "stream",
|
||||
"_changed", "_new", "_ignore_errors")
|
||||
|
||||
HeaderClass = header.DbfHeader
|
||||
RecordClass = record.DbfRecord
|
||||
INVALID_VALUE = INVALID_VALUE
|
||||
|
||||
# initialization and creation helpers
|
||||
|
||||
def __init__(self, f, readOnly=False, new=False, ignoreErrors=False):
|
||||
"""Initialize instance.
|
||||
|
||||
Arguments:
|
||||
f:
|
||||
Filename or file-like object.
|
||||
new:
|
||||
True if new data table must be created. Assume
|
||||
data table exists if this argument is False.
|
||||
readOnly:
|
||||
if ``f`` argument is a string file will
|
||||
be opend in read-only mode; in other cases
|
||||
this argument is ignored. This argument is ignored
|
||||
even if ``new`` argument is True.
|
||||
headerObj:
|
||||
`header.DbfHeader` instance or None. If this argument
|
||||
is None, new empty header will be used with the
|
||||
all fields set by default.
|
||||
ignoreErrors:
|
||||
if set, failing field value conversion will return
|
||||
``INVALID_VALUE`` instead of raising conversion error.
|
||||
|
||||
"""
|
||||
if isinstance(f, str):
|
||||
# a filename
|
||||
self.name = f
|
||||
if new:
|
||||
# new table (table file must be
|
||||
# created or opened and truncated)
|
||||
self.stream = open(f, "w+b")
|
||||
else:
|
||||
# tabe file must exist
|
||||
self.stream = open(f, ("r+b", "rb")[bool(readOnly)])
|
||||
else:
|
||||
# a stream
|
||||
self.name = getattr(f, "name", "")
|
||||
self.stream = f
|
||||
if new:
|
||||
# if this is a new table, header will be empty
|
||||
self.header = self.HeaderClass()
|
||||
else:
|
||||
# or instantiated using stream
|
||||
self.header = self.HeaderClass.fromStream(self.stream)
|
||||
self.ignoreErrors = ignoreErrors
|
||||
self._new = bool(new)
|
||||
self._changed = False
|
||||
|
||||
# properties
|
||||
|
||||
closed = property(lambda self: self.stream.closed)
|
||||
recordCount = property(lambda self: self.header.recordCount)
|
||||
fieldNames = property(
|
||||
lambda self: [_fld.name for _fld in self.header.fields])
|
||||
fieldDefs = property(lambda self: self.header.fields)
|
||||
changed = property(lambda self: self._changed or self.header.changed)
|
||||
|
||||
def ignoreErrors(self, value):
|
||||
"""Update `ignoreErrors` flag on the header object and self"""
|
||||
self.header.ignoreErrors = self._ignore_errors = bool(value)
|
||||
|
||||
ignoreErrors = property(
|
||||
lambda self: self._ignore_errors,
|
||||
ignoreErrors,
|
||||
doc="""Error processing mode for DBF field value conversion
|
||||
|
||||
if set, failing field value conversion will return
|
||||
``INVALID_VALUE`` instead of raising conversion error.
|
||||
|
||||
""")
|
||||
|
||||
# protected methods
|
||||
|
||||
def _fixIndex(self, index):
|
||||
"""Return fixed index.
|
||||
|
||||
This method fails if index isn't a numeric object
|
||||
(long or int). Or index isn't in a valid range
|
||||
(less or equal to the number of records in the db).
|
||||
|
||||
If ``index`` is a negative number, it will be
|
||||
treated as a negative indexes for list objects.
|
||||
|
||||
Return:
|
||||
Return value is numeric object maning valid index.
|
||||
|
||||
"""
|
||||
if not isinstance(index, int):
|
||||
raise TypeError("Index must be a numeric object")
|
||||
if index < 0:
|
||||
# index from the right side
|
||||
# fix it to the left-side index
|
||||
index += len(self) + 1
|
||||
if index >= len(self):
|
||||
raise IndexError("Record index out of range")
|
||||
return index
|
||||
|
||||
# iterface methods
|
||||
|
||||
def close(self):
|
||||
self.flush()
|
||||
self.stream.close()
|
||||
|
||||
def flush(self):
|
||||
"""Flush data to the associated stream."""
|
||||
if self.changed:
|
||||
self.header.setCurrentDate()
|
||||
self.header.write(self.stream)
|
||||
self.stream.flush()
|
||||
self._changed = False
|
||||
|
||||
def indexOfFieldName(self, name):
|
||||
"""Index of field named ``name``."""
|
||||
# FIXME: move this to header class
|
||||
names = [f.name for f in self.header.fields]
|
||||
return names.index(name.upper())
|
||||
|
||||
def newRecord(self):
|
||||
"""Return new record, which belong to this table."""
|
||||
return self.RecordClass(self)
|
||||
|
||||
def append(self, record):
|
||||
"""Append ``record`` to the database."""
|
||||
record.index = self.header.recordCount
|
||||
record._write()
|
||||
self.header.recordCount += 1
|
||||
self._changed = True
|
||||
self._new = False
|
||||
|
||||
def addField(self, *defs):
|
||||
"""Add field definitions.
|
||||
|
||||
For more information see `header.DbfHeader.addField`.
|
||||
|
||||
"""
|
||||
if self._new:
|
||||
self.header.addField(*defs)
|
||||
else:
|
||||
raise TypeError("At least one record was added, "
|
||||
"structure can't be changed")
|
||||
|
||||
# 'magic' methods (representation and sequence interface)
|
||||
|
||||
def __repr__(self):
|
||||
return "Dbf stream '%s'\n" % self.stream + repr(self.header)
|
||||
|
||||
def __len__(self):
|
||||
"""Return number of records."""
|
||||
return self.recordCount
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Return `DbfRecord` instance."""
|
||||
return self.RecordClass.fromStream(self, self._fixIndex(index))
|
||||
|
||||
def __setitem__(self, index, record):
|
||||
"""Write `DbfRecord` instance to the stream."""
|
||||
record.index = self._fixIndex(index)
|
||||
record._write()
|
||||
self._changed = True
|
||||
self._new = False
|
||||
|
||||
# def __del__(self):
|
||||
# """Flush stream upon deletion of the object."""
|
||||
# self.flush()
|
||||
|
||||
|
||||
def demo_read(filename):
|
||||
_dbf = Dbf(filename, True)
|
||||
for _rec in _dbf:
|
||||
print()
|
||||
print(repr(_rec))
|
||||
_dbf.close()
|
||||
|
||||
|
||||
def demo_create(filename):
|
||||
_dbf = Dbf(filename, new=True)
|
||||
_dbf.addField(
|
||||
("NAME", "C", 15),
|
||||
("SURNAME", "C", 25),
|
||||
("INITIALS", "C", 10),
|
||||
("BIRTHDATE", "D"),
|
||||
)
|
||||
for (_n, _s, _i, _b) in (
|
||||
("John", "Miller", "YC", (1981, 1, 2)),
|
||||
("Andy", "Larkin", "AL", (1982, 3, 4)),
|
||||
("Bill", "Clinth", "", (1983, 5, 6)),
|
||||
("Bobb", "McNail", "", (1984, 7, 8)),
|
||||
):
|
||||
_rec = _dbf.newRecord()
|
||||
_rec["NAME"] = _n
|
||||
_rec["SURNAME"] = _s
|
||||
_rec["INITIALS"] = _i
|
||||
_rec["BIRTHDATE"] = _b
|
||||
_rec.store()
|
||||
print(repr(_dbf))
|
||||
_dbf.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
_name = len(sys.argv) > 1 and sys.argv[1] or "county.dbf"
|
||||
demo_create(_name)
|
||||
demo_read(_name)
|
||||
|
||||
# vim: set et sw=4 sts=4 :
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/python
|
||||
""".DBF creation helpers.
|
||||
|
||||
Note: this is a legacy interface. New code should use Dbf class
|
||||
for table creation (see examples in dbf.py)
|
||||
|
||||
TODO:
|
||||
- handle Memo fields.
|
||||
- check length of the fields accoring to the
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
|
||||
"""
|
||||
"""History (most recent first)
|
||||
04-jul-2006 [als] added export declaration;
|
||||
updated for dbfpy 2.0
|
||||
15-dec-2005 [yc] define dbf_new.__slots__
|
||||
14-dec-2005 [yc] added vim modeline; retab'd; added doc-strings;
|
||||
dbf_new now is a new class (inherited from object)
|
||||
??-jun-2000 [--] added by Hans Fiby
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.4 $"[11:-2]
|
||||
__date__ = "$Date: 2006/07/04 08:18:18 $"[7:-2]
|
||||
|
||||
__all__ = ["dbf_new"]
|
||||
|
||||
from .dbf import *
|
||||
from .fields import *
|
||||
from .header import *
|
||||
from .record import *
|
||||
|
||||
|
||||
class _FieldDefinition(object):
|
||||
"""Field definition.
|
||||
|
||||
This is a simple structure, which contains ``name``, ``type``,
|
||||
``len``, ``dec`` and ``cls`` fields.
|
||||
|
||||
Objects also implement get/setitem magic functions, so fields
|
||||
could be accessed via sequence iterface, where 'name' has
|
||||
index 0, 'type' index 1, 'len' index 2, 'dec' index 3 and
|
||||
'cls' could be located at index 4.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = "name", "type", "len", "dec", "cls"
|
||||
|
||||
# WARNING: be attentive - dictionaries are mutable!
|
||||
FLD_TYPES = {
|
||||
# type: (cls, len)
|
||||
"C": (DbfCharacterFieldDef, None),
|
||||
"N": (DbfNumericFieldDef, None),
|
||||
"L": (DbfLogicalFieldDef, 1),
|
||||
# FIXME: support memos
|
||||
# "M": (DbfMemoFieldDef),
|
||||
"D": (DbfDateFieldDef, 8),
|
||||
# FIXME: I'm not sure length should be 14 characters!
|
||||
# but temporary I use it, cuz date is 8 characters
|
||||
# and time 6 (hhmmss)
|
||||
"T": (DbfDateTimeFieldDef, 14),
|
||||
}
|
||||
|
||||
def __init__(self, name, type, len=None, dec=0):
|
||||
_cls, _len = self.FLD_TYPES[type]
|
||||
if _len is None:
|
||||
if len is None:
|
||||
raise ValueError("Field length must be defined")
|
||||
_len = len
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.len = _len
|
||||
self.dec = dec
|
||||
self.cls = _cls
|
||||
|
||||
def getDbfField(self):
|
||||
"Return `DbfFieldDef` instance from the current definition."
|
||||
return self.cls(self.name, self.len, self.dec)
|
||||
|
||||
def appendToHeader(self, dbfh):
|
||||
"""Create a `DbfFieldDef` instance and append it to the dbf header.
|
||||
|
||||
Arguments:
|
||||
dbfh: `DbfHeader` instance.
|
||||
|
||||
"""
|
||||
_dbff = self.getDbfField()
|
||||
dbfh.addField(_dbff)
|
||||
|
||||
|
||||
class dbf_new(object):
|
||||
"""New .DBF creation helper.
|
||||
|
||||
Example Usage:
|
||||
|
||||
dbfn = dbf_new()
|
||||
dbfn.add_field("name",'C',80)
|
||||
dbfn.add_field("price",'N',10,2)
|
||||
dbfn.add_field("date",'D',8)
|
||||
dbfn.write("tst.dbf")
|
||||
|
||||
Note:
|
||||
This module cannot handle Memo-fields,
|
||||
they are special.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("fields",)
|
||||
|
||||
FieldDefinitionClass = _FieldDefinition
|
||||
|
||||
def __init__(self):
|
||||
self.fields = []
|
||||
|
||||
def add_field(self, name, typ, len, dec=0):
|
||||
"""Add field definition.
|
||||
|
||||
Arguments:
|
||||
name:
|
||||
field name (str object). field name must not
|
||||
contain ASCII NULs and it's length shouldn't
|
||||
exceed 10 characters.
|
||||
typ:
|
||||
type of the field. this must be a single character
|
||||
from the "CNLMDT" set meaning character, numeric,
|
||||
logical, memo, date and date/time respectively.
|
||||
len:
|
||||
length of the field. this argument is used only for
|
||||
the character and numeric fields. all other fields
|
||||
have fixed length.
|
||||
FIXME: use None as a default for this argument?
|
||||
dec:
|
||||
decimal precision. used only for the numric fields.
|
||||
|
||||
"""
|
||||
self.fields.append(self.FieldDefinitionClass(name, typ, len, dec))
|
||||
|
||||
def write(self, filename):
|
||||
"""Create empty .DBF file using current structure."""
|
||||
_dbfh = DbfHeader()
|
||||
_dbfh.setCurrentDate()
|
||||
for _fldDef in self.fields:
|
||||
_fldDef.appendToHeader(_dbfh)
|
||||
|
||||
_dbfStream = open(filename, "wb")
|
||||
_dbfh.write(_dbfStream)
|
||||
_dbfStream.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# create a new DBF-File
|
||||
dbfn = dbf_new()
|
||||
dbfn.add_field("name", 'C', 80)
|
||||
dbfn.add_field("price", 'N', 10, 2)
|
||||
dbfn.add_field("date", 'D', 8)
|
||||
dbfn.write("tst.dbf")
|
||||
# test new dbf
|
||||
print("*** created tst.dbf: ***")
|
||||
dbft = Dbf('tst.dbf', readOnly=0)
|
||||
print(repr(dbft))
|
||||
# add a record
|
||||
rec = DbfRecord(dbft)
|
||||
rec['name'] = 'something'
|
||||
rec['price'] = 10.5
|
||||
rec['date'] = (2000, 1, 12)
|
||||
rec.store()
|
||||
# add another record
|
||||
rec = DbfRecord(dbft)
|
||||
rec['name'] = 'foo and bar'
|
||||
rec['price'] = 12234
|
||||
rec['date'] = (1992, 7, 15)
|
||||
rec.store()
|
||||
|
||||
# show the records
|
||||
print("*** inserted 2 records into tst.dbf: ***")
|
||||
print(repr(dbft))
|
||||
for i1 in range(len(dbft)):
|
||||
rec = dbft[i1]
|
||||
for fldName in dbft.fieldNames:
|
||||
print('%s:\t %s' % (fldName, rec[fldName]))
|
||||
print()
|
||||
dbft.close()
|
||||
|
||||
# vim: set et sts=4 sw=4 :
|
||||
@@ -0,0 +1,467 @@
|
||||
"""DBF fields definitions.
|
||||
|
||||
TODO:
|
||||
- make memos work
|
||||
"""
|
||||
"""History (most recent first):
|
||||
26-may-2009 [als] DbfNumericFieldDef.decodeValue: strip zero bytes
|
||||
05-feb-2009 [als] DbfDateFieldDef.encodeValue: empty arg produces empty date
|
||||
16-sep-2008 [als] DbfNumericFieldDef decoding looks for decimal point
|
||||
in the value to select float or integer return type
|
||||
13-mar-2008 [als] check field name length in constructor
|
||||
11-feb-2007 [als] handle value conversion errors
|
||||
10-feb-2007 [als] DbfFieldDef: added .rawFromRecord()
|
||||
01-dec-2006 [als] Timestamp columns use None for empty values
|
||||
31-oct-2006 [als] support field types 'F' (float), 'I' (integer)
|
||||
and 'Y' (currency);
|
||||
automate export and registration of field classes
|
||||
04-jul-2006 [als] added export declaration
|
||||
10-mar-2006 [als] decode empty values for Date and Logical fields;
|
||||
show field name in errors
|
||||
10-mar-2006 [als] fix Numeric value decoding: according to spec,
|
||||
value always is string representation of the number;
|
||||
ensure that encoded Numeric value fits into the field
|
||||
20-dec-2005 [yc] use field names in upper case
|
||||
15-dec-2005 [yc] field definitions moved from `dbf`.
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.14 $"[11:-2]
|
||||
__date__ = "$Date: 2009/05/26 05:16:51 $"[7:-2]
|
||||
|
||||
__all__ = ["lookupFor",] # field classes added at the end of the module
|
||||
|
||||
import datetime
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from . import utils
|
||||
|
||||
## abstract definitions
|
||||
|
||||
class DbfFieldDef(object):
|
||||
"""Abstract field definition.
|
||||
|
||||
Child classes must override ``type`` class attribute to provide datatype
|
||||
infromation of the field definition. For more info about types visit
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
|
||||
Also child classes must override ``defaultValue`` field to provide
|
||||
default value for the field value.
|
||||
|
||||
If child class has fixed length ``length`` class attribute must be
|
||||
overriden and set to the valid value. None value means, that field
|
||||
isn't of fixed length.
|
||||
|
||||
Note: ``name`` field must not be changed after instantiation.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
__slots__ = ("name", "decimalCount",
|
||||
"start", "end", "ignoreErrors")
|
||||
|
||||
# length of the field, None in case of variable-length field,
|
||||
# or a number if this field is a fixed-length field
|
||||
length = None
|
||||
|
||||
# field type. for more information about fields types visit
|
||||
# `http://www.clicketyclick.dk/databases/xbase/format/data_types.html`
|
||||
# must be overriden in child classes
|
||||
typeCode = None
|
||||
|
||||
# default value for the field. this field must be
|
||||
# overriden in child classes
|
||||
defaultValue = None
|
||||
|
||||
def __init__(self, name, length=None, decimalCount=None,
|
||||
start=None, stop=None, ignoreErrors=False,
|
||||
):
|
||||
"""Initialize instance."""
|
||||
assert self.typeCode is not None, "Type code must be overriden"
|
||||
assert self.defaultValue is not None, "Default value must be overriden"
|
||||
## fix arguments
|
||||
if len(name) >10:
|
||||
raise ValueError("Field name \"%s\" is too long" % name)
|
||||
name = str(name).upper()
|
||||
if self.__class__.length is None:
|
||||
if length is None:
|
||||
raise ValueError("[%s] Length isn't specified" % name)
|
||||
length = int(length)
|
||||
if length <= 0:
|
||||
raise ValueError("[%s] Length must be a positive integer"
|
||||
% name)
|
||||
else:
|
||||
length = self.length
|
||||
if decimalCount is None:
|
||||
decimalCount = 0
|
||||
## set fields
|
||||
self.name = name
|
||||
# FIXME: validate length according to the specification at
|
||||
# http://www.clicketyclick.dk/databases/xbase/format/data_types.html
|
||||
self.length = length
|
||||
self.decimalCount = decimalCount
|
||||
self.ignoreErrors = ignoreErrors
|
||||
self.start = start
|
||||
self.end = stop
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.name, str(other).upper())
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def fromString(cls, string, start, ignoreErrors=False):
|
||||
"""Decode dbf field definition from the string data.
|
||||
|
||||
Arguments:
|
||||
string:
|
||||
a string, dbf definition is decoded from. length of
|
||||
the string must be 32 bytes.
|
||||
start:
|
||||
position in the database file.
|
||||
ignoreErrors:
|
||||
initial error processing mode for the new field (boolean)
|
||||
|
||||
"""
|
||||
assert len(string) == 32
|
||||
_length = string[16]
|
||||
return cls(utils.unzfill(string)[:11].decode('utf-8'), _length,
|
||||
string[17], start, start + _length, ignoreErrors=ignoreErrors)
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
def toString(self):
|
||||
"""Return encoded field definition.
|
||||
|
||||
Return:
|
||||
Return value is a string object containing encoded
|
||||
definition of this field.
|
||||
|
||||
"""
|
||||
if sys.version_info < (2, 4):
|
||||
# earlier versions did not support padding character
|
||||
_name = self.name[:11] + "\0" * (11 - len(self.name))
|
||||
else:
|
||||
_name = self.name.ljust(11, '\0')
|
||||
return (
|
||||
_name +
|
||||
self.typeCode +
|
||||
#data address
|
||||
chr(0) * 4 +
|
||||
chr(self.length) +
|
||||
chr(self.decimalCount) +
|
||||
chr(0) * 14
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "%-10s %1s %3d %3d" % self.fieldInfo()
|
||||
|
||||
def fieldInfo(self):
|
||||
"""Return field information.
|
||||
|
||||
Return:
|
||||
Return value is a (name, type, length, decimals) tuple.
|
||||
|
||||
"""
|
||||
return (self.name, self.typeCode, self.length, self.decimalCount)
|
||||
|
||||
def rawFromRecord(self, record):
|
||||
"""Return a "raw" field value from the record string."""
|
||||
return record[self.start:self.end]
|
||||
|
||||
def decodeFromRecord(self, record):
|
||||
"""Return decoded field value from the record string."""
|
||||
try:
|
||||
return self.decodeValue(self.rawFromRecord(record))
|
||||
except:
|
||||
if self.ignoreErrors:
|
||||
return utils.INVALID_VALUE
|
||||
else:
|
||||
raise
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return decoded value from string value.
|
||||
|
||||
This method shouldn't be used publicly. It's called from the
|
||||
`decodeFromRecord` method.
|
||||
|
||||
This is an abstract method and it must be overridden in child classes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return str object containing encoded field value.
|
||||
|
||||
This is an abstract method and it must be overriden in child classes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
## real classes
|
||||
|
||||
class DbfCharacterFieldDef(DbfFieldDef):
|
||||
"""Definition of the character field."""
|
||||
|
||||
typeCode = "C"
|
||||
defaultValue = b''
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return string object.
|
||||
|
||||
Return value is a ``value`` argument with stripped right spaces.
|
||||
|
||||
"""
|
||||
return value.rstrip(b' ').decode('utf-8')
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return raw data string encoded from a ``value``."""
|
||||
return str(value)[:self.length].ljust(self.length)
|
||||
|
||||
|
||||
class DbfNumericFieldDef(DbfFieldDef):
|
||||
"""Definition of the numeric field."""
|
||||
|
||||
typeCode = "N"
|
||||
# XXX: now I'm not sure it was a good idea to make a class field
|
||||
# `defaultValue` instead of a generic method as it was implemented
|
||||
# previously -- it's ok with all types except number, cuz
|
||||
# if self.decimalCount is 0, we should return 0 and 0.0 otherwise.
|
||||
defaultValue = 0
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return a number decoded from ``value``.
|
||||
|
||||
If decimals is zero, value will be decoded as an integer;
|
||||
or as a float otherwise.
|
||||
|
||||
Return:
|
||||
Return value is a int (long) or float instance.
|
||||
|
||||
"""
|
||||
value = value.strip(b' \0')
|
||||
if b'.' in value:
|
||||
# a float (has decimal separator)
|
||||
return float(value)
|
||||
elif value:
|
||||
# must be an integer
|
||||
return int(value)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return string containing encoded ``value``."""
|
||||
_rv = ("%*.*f" % (self.length, self.decimalCount, value))
|
||||
if len(_rv) > self.length:
|
||||
_ppos = _rv.find(".")
|
||||
if 0 <= _ppos <= self.length:
|
||||
_rv = _rv[:self.length]
|
||||
else:
|
||||
raise ValueError("[%s] Numeric overflow: %s (field width: %i)"
|
||||
% (self.name, _rv, self.length))
|
||||
return _rv
|
||||
|
||||
class DbfFloatFieldDef(DbfNumericFieldDef):
|
||||
"""Definition of the float field - same as numeric."""
|
||||
|
||||
typeCode = "F"
|
||||
|
||||
class DbfIntegerFieldDef(DbfFieldDef):
|
||||
"""Definition of the integer field."""
|
||||
|
||||
typeCode = "I"
|
||||
length = 4
|
||||
defaultValue = 0
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return an integer number decoded from ``value``."""
|
||||
return struct.unpack("<i", value)[0]
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return string containing encoded ``value``."""
|
||||
return struct.pack("<i", int(value))
|
||||
|
||||
class DbfCurrencyFieldDef(DbfFieldDef):
|
||||
"""Definition of the currency field."""
|
||||
|
||||
typeCode = "Y"
|
||||
length = 8
|
||||
defaultValue = 0.0
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return float number decoded from ``value``."""
|
||||
return struct.unpack("<q", value)[0] / 10000.
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return string containing encoded ``value``."""
|
||||
return struct.pack("<q", round(value * 10000))
|
||||
|
||||
class DbfLogicalFieldDef(DbfFieldDef):
|
||||
"""Definition of the logical field."""
|
||||
|
||||
typeCode = "L"
|
||||
defaultValue = -1
|
||||
length = 1
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return True, False or -1 decoded from ``value``."""
|
||||
# Note: value always is 1-char string
|
||||
if value == "?":
|
||||
return -1
|
||||
if value in "NnFf ":
|
||||
return False
|
||||
if value in "YyTt":
|
||||
return True
|
||||
raise ValueError("[%s] Invalid logical value %r" % (self.name, value))
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return a character from the "TF?" set.
|
||||
|
||||
Return:
|
||||
Return value is "T" if ``value`` is True
|
||||
"?" if value is -1 or False otherwise.
|
||||
|
||||
"""
|
||||
if value is True:
|
||||
return "T"
|
||||
if value == -1:
|
||||
return "?"
|
||||
return "F"
|
||||
|
||||
|
||||
class DbfMemoFieldDef(DbfFieldDef):
|
||||
"""Definition of the memo field.
|
||||
|
||||
Note: memos aren't currenly completely supported.
|
||||
|
||||
"""
|
||||
|
||||
typeCode = "M"
|
||||
defaultValue = " " * 10
|
||||
length = 10
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return int .dbt block number decoded from the string object."""
|
||||
#return int(value)
|
||||
raise NotImplementedError
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return raw data string encoded from a ``value``.
|
||||
|
||||
Note: this is an internal method.
|
||||
|
||||
"""
|
||||
#return str(value)[:self.length].ljust(self.length)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DbfDateFieldDef(DbfFieldDef):
|
||||
"""Definition of the date field."""
|
||||
|
||||
typeCode = "D"
|
||||
defaultValue = utils.classproperty(lambda cls: datetime.date.today())
|
||||
# "yyyymmdd" gives us 8 characters
|
||||
length = 8
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return a ``datetime.date`` instance decoded from ``value``."""
|
||||
if value.strip():
|
||||
return utils.getDate(value)
|
||||
else:
|
||||
return None
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return a string-encoded value.
|
||||
|
||||
``value`` argument should be a value suitable for the
|
||||
`utils.getDate` call.
|
||||
|
||||
Return:
|
||||
Return value is a string in format "yyyymmdd".
|
||||
|
||||
"""
|
||||
if value:
|
||||
return utils.getDate(value).strftime("%Y%m%d")
|
||||
else:
|
||||
return " " * self.length
|
||||
|
||||
|
||||
class DbfDateTimeFieldDef(DbfFieldDef):
|
||||
"""Definition of the timestamp field."""
|
||||
|
||||
# a difference between JDN (Julian Day Number)
|
||||
# and GDN (Gregorian Day Number). note, that GDN < JDN
|
||||
JDN_GDN_DIFF = 1721425
|
||||
typeCode = "T"
|
||||
defaultValue = utils.classproperty(lambda cls: datetime.datetime.now())
|
||||
# two 32-bits integers representing JDN and amount of
|
||||
# milliseconds respectively gives us 8 bytes.
|
||||
# note, that values must be encoded in LE byteorder.
|
||||
length = 8
|
||||
|
||||
def decodeValue(self, value):
|
||||
"""Return a `datetime.datetime` instance."""
|
||||
assert len(value) == self.length
|
||||
# LE byteorder
|
||||
_jdn, _msecs = struct.unpack("<2I", value)
|
||||
if _jdn >= 1:
|
||||
_rv = datetime.datetime.fromordinal(_jdn - self.JDN_GDN_DIFF)
|
||||
_rv += datetime.timedelta(0, _msecs / 1000.0)
|
||||
else:
|
||||
# empty date
|
||||
_rv = None
|
||||
return _rv
|
||||
|
||||
def encodeValue(self, value):
|
||||
"""Return a string-encoded ``value``."""
|
||||
if value:
|
||||
value = utils.getDateTime(value)
|
||||
# LE byteorder
|
||||
_rv = struct.pack("<2I", value.toordinal() + self.JDN_GDN_DIFF,
|
||||
(value.hour * 3600 + value.minute * 60 + value.second) * 1000)
|
||||
else:
|
||||
_rv = "\0" * self.length
|
||||
assert len(_rv) == self.length
|
||||
return _rv
|
||||
|
||||
|
||||
_fieldsRegistry = {}
|
||||
|
||||
def registerField(fieldCls):
|
||||
"""Register field definition class.
|
||||
|
||||
``fieldCls`` should be subclass of the `DbfFieldDef`.
|
||||
|
||||
Use `lookupFor` to retrieve field definition class
|
||||
by the type code.
|
||||
|
||||
"""
|
||||
assert fieldCls.typeCode is not None, "Type code isn't defined"
|
||||
# XXX: use fieldCls.typeCode.upper()? in case of any decign
|
||||
# don't forget to look to the same comment in ``lookupFor`` method
|
||||
_fieldsRegistry[fieldCls.typeCode] = fieldCls
|
||||
|
||||
|
||||
def lookupFor(typeCode):
|
||||
"""Return field definition class for the given type code.
|
||||
|
||||
``typeCode`` must be a single character. That type should be
|
||||
previously registered.
|
||||
|
||||
Use `registerField` to register new field class.
|
||||
|
||||
Return:
|
||||
Return value is a subclass of the `DbfFieldDef`.
|
||||
|
||||
"""
|
||||
# XXX: use typeCode.upper()? in case of any decign don't
|
||||
# forget to look to the same comment in ``registerField``
|
||||
return _fieldsRegistry[chr(typeCode)]
|
||||
|
||||
## register generic types
|
||||
|
||||
for (_name, _val) in list(globals().items()):
|
||||
if isinstance(_val, type) and issubclass(_val, DbfFieldDef) \
|
||||
and (_name != "DbfFieldDef"):
|
||||
__all__.append(_name)
|
||||
registerField(_val)
|
||||
del _name, _val
|
||||
|
||||
# vim: et sts=4 sw=4 :
|
||||
@@ -0,0 +1,273 @@
|
||||
"""DBF header definition.
|
||||
|
||||
TODO:
|
||||
- handle encoding of the character fields
|
||||
(encoding information stored in the DBF header)
|
||||
|
||||
"""
|
||||
"""History (most recent first):
|
||||
16-sep-2010 [als] fromStream: fix century of the last update field
|
||||
11-feb-2007 [als] added .ignoreErrors
|
||||
10-feb-2007 [als] added __getitem__: return field definitions
|
||||
by field name or field number (zero-based)
|
||||
04-jul-2006 [als] added export declaration
|
||||
15-dec-2005 [yc] created
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.6 $"[11:-2]
|
||||
__date__ = "$Date: 2010/09/16 05:06:39 $"[7:-2]
|
||||
|
||||
__all__ = ["DbfHeader"]
|
||||
|
||||
import io
|
||||
import datetime
|
||||
import struct
|
||||
import time
|
||||
import sys
|
||||
|
||||
from . import fields
|
||||
from .utils import getDate
|
||||
|
||||
|
||||
class DbfHeader(object):
|
||||
"""Dbf header definition.
|
||||
|
||||
For more information about dbf header format visit
|
||||
`http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_STRUCT`
|
||||
|
||||
Examples:
|
||||
Create an empty dbf header and add some field definitions:
|
||||
dbfh = DbfHeader()
|
||||
dbfh.addField(("name", "C", 10))
|
||||
dbfh.addField(("date", "D"))
|
||||
dbfh.addField(DbfNumericFieldDef("price", 5, 2))
|
||||
Create a dbf header with field definitions:
|
||||
dbfh = DbfHeader([
|
||||
("name", "C", 10),
|
||||
("date", "D"),
|
||||
DbfNumericFieldDef("price", 5, 2),
|
||||
])
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("signature", "fields", "lastUpdate", "recordLength",
|
||||
"recordCount", "headerLength", "changed", "_ignore_errors")
|
||||
|
||||
## instance construction and initialization methods
|
||||
|
||||
def __init__(self, fields=None, headerLength=0, recordLength=0,
|
||||
recordCount=0, signature=0x03, lastUpdate=None, ignoreErrors=False,
|
||||
):
|
||||
"""Initialize instance.
|
||||
|
||||
Arguments:
|
||||
fields:
|
||||
a list of field definitions;
|
||||
recordLength:
|
||||
size of the records;
|
||||
headerLength:
|
||||
size of the header;
|
||||
recordCount:
|
||||
number of records stored in DBF;
|
||||
signature:
|
||||
version number (aka signature). using 0x03 as a default meaning
|
||||
"File without DBT". for more information about this field visit
|
||||
``http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_NOTE_1_TARGET``
|
||||
lastUpdate:
|
||||
date of the DBF's update. this could be a string ('yymmdd' or
|
||||
'yyyymmdd'), timestamp (int or float), datetime/date value,
|
||||
a sequence (assuming (yyyy, mm, dd, ...)) or an object having
|
||||
callable ``ticks`` field.
|
||||
ignoreErrors:
|
||||
error processing mode for DBF fields (boolean)
|
||||
|
||||
"""
|
||||
self.signature = signature
|
||||
if fields is None:
|
||||
self.fields = []
|
||||
else:
|
||||
self.fields = list(fields)
|
||||
self.lastUpdate = getDate(lastUpdate)
|
||||
self.recordLength = recordLength
|
||||
self.headerLength = headerLength
|
||||
self.recordCount = recordCount
|
||||
self.ignoreErrors = ignoreErrors
|
||||
# XXX: I'm not sure this is safe to
|
||||
# initialize `self.changed` in this way
|
||||
self.changed = bool(self.fields)
|
||||
|
||||
# @classmethod
|
||||
def fromString(cls, string):
|
||||
"""Return header instance from the string object."""
|
||||
return cls.fromStream(io.StringIO(str(string)))
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
# @classmethod
|
||||
def fromStream(cls, stream):
|
||||
"""Return header object from the stream."""
|
||||
stream.seek(0)
|
||||
first_32 = stream.read(32)
|
||||
if type(first_32) != bytes:
|
||||
_data = bytes(first_32, sys.getfilesystemencoding())
|
||||
_data = first_32
|
||||
(_cnt, _hdrLen, _recLen) = struct.unpack("<I2H", _data[4:12])
|
||||
#reserved = _data[12:32]
|
||||
_year = _data[1]
|
||||
if _year < 80:
|
||||
# dBase II started at 1980. It is quite unlikely
|
||||
# that actual last update date is before that year.
|
||||
_year += 2000
|
||||
else:
|
||||
_year += 1900
|
||||
## create header object
|
||||
_obj = cls(None, _hdrLen, _recLen, _cnt, _data[0],
|
||||
(_year, _data[2], _data[3]))
|
||||
## append field definitions
|
||||
# position 0 is for the deletion flag
|
||||
_pos = 1
|
||||
_data = stream.read(1)
|
||||
while _data != b'\r':
|
||||
_data += stream.read(31)
|
||||
_fld = fields.lookupFor(_data[11]).fromString(_data, _pos)
|
||||
_obj._addField(_fld)
|
||||
_pos = _fld.end
|
||||
_data = stream.read(1)
|
||||
return _obj
|
||||
fromStream = classmethod(fromStream)
|
||||
|
||||
## properties
|
||||
|
||||
year = property(lambda self: self.lastUpdate.year)
|
||||
month = property(lambda self: self.lastUpdate.month)
|
||||
day = property(lambda self: self.lastUpdate.day)
|
||||
|
||||
def ignoreErrors(self, value):
|
||||
"""Update `ignoreErrors` flag on self and all fields"""
|
||||
self._ignore_errors = value = bool(value)
|
||||
for _field in self.fields:
|
||||
_field.ignoreErrors = value
|
||||
ignoreErrors = property(
|
||||
lambda self: self._ignore_errors,
|
||||
ignoreErrors,
|
||||
doc="""Error processing mode for DBF field value conversion
|
||||
|
||||
if set, failing field value conversion will return
|
||||
``INVALID_VALUE`` instead of raising conversion error.
|
||||
|
||||
""")
|
||||
|
||||
## object representation
|
||||
|
||||
def __repr__(self):
|
||||
_rv = """\
|
||||
Version (signature): 0x%02x
|
||||
Last update: %s
|
||||
Header length: %d
|
||||
Record length: %d
|
||||
Record count: %d
|
||||
FieldName Type Len Dec
|
||||
""" % (self.signature, self.lastUpdate, self.headerLength,
|
||||
self.recordLength, self.recordCount)
|
||||
_rv += "\n".join(
|
||||
["%10s %4s %3s %3s" % _fld.fieldInfo() for _fld in self.fields]
|
||||
)
|
||||
return _rv
|
||||
|
||||
## internal methods
|
||||
|
||||
def _addField(self, *defs):
|
||||
"""Internal variant of the `addField` method.
|
||||
|
||||
This method doesn't set `self.changed` field to True.
|
||||
|
||||
Return value is a length of the appended records.
|
||||
Note: this method doesn't modify ``recordLength`` and
|
||||
``headerLength`` fields. Use `addField` instead of this
|
||||
method if you don't exactly know what you're doing.
|
||||
|
||||
"""
|
||||
# insure we have dbf.DbfFieldDef instances first (instantiation
|
||||
# from the tuple could raise an error, in such a case I don't
|
||||
# wanna add any of the definitions -- all will be ignored)
|
||||
_defs = []
|
||||
_recordLength = 0
|
||||
for _def in defs:
|
||||
if isinstance(_def, fields.DbfFieldDef):
|
||||
_obj = _def
|
||||
else:
|
||||
(_name, _type, _len, _dec) = (tuple(_def) + (None,) * 4)[:4]
|
||||
_cls = fields.lookupFor(_type)
|
||||
_obj = _cls(_name, _len, _dec,
|
||||
ignoreErrors=self._ignore_errors)
|
||||
_recordLength += _obj.length
|
||||
_defs.append(_obj)
|
||||
# and now extend field definitions and
|
||||
# update record length
|
||||
self.fields += _defs
|
||||
return _recordLength
|
||||
|
||||
## interface methods
|
||||
|
||||
def addField(self, *defs):
|
||||
"""Add field definition to the header.
|
||||
|
||||
Examples:
|
||||
dbfh.addField(
|
||||
("name", "C", 20),
|
||||
dbf.DbfCharacterFieldDef("surname", 20),
|
||||
dbf.DbfDateFieldDef("birthdate"),
|
||||
("member", "L"),
|
||||
)
|
||||
dbfh.addField(("price", "N", 5, 2))
|
||||
dbfh.addField(dbf.DbfNumericFieldDef("origprice", 5, 2))
|
||||
|
||||
"""
|
||||
_oldLen = self.recordLength
|
||||
self.recordLength += self._addField(*defs)
|
||||
if not _oldLen:
|
||||
self.recordLength += 1
|
||||
# XXX: may be just use:
|
||||
# self.recordeLength += self._addField(*defs) + bool(not _oldLen)
|
||||
# recalculate headerLength
|
||||
self.headerLength = 32 + (32 * len(self.fields)) + 1
|
||||
self.changed = True
|
||||
|
||||
def write(self, stream):
|
||||
"""Encode and write header to the stream."""
|
||||
stream.seek(0)
|
||||
stream.write(self.toString())
|
||||
fields = [_fld.toString() for _fld in self.fields]
|
||||
stream.write(''.join(fields).encode(sys.getfilesystemencoding()))
|
||||
stream.write(b'\x0D') # cr at end of all header data
|
||||
self.changed = False
|
||||
|
||||
def toString(self):
|
||||
"""Returned 32 chars length string with encoded header."""
|
||||
return struct.pack("<4BI2H",
|
||||
self.signature,
|
||||
self.year - 1900,
|
||||
self.month,
|
||||
self.day,
|
||||
self.recordCount,
|
||||
self.headerLength,
|
||||
self.recordLength) + (b'\x00' * 20)
|
||||
#TODO: figure out if bytes(utf-8) is correct here.
|
||||
|
||||
def setCurrentDate(self):
|
||||
"""Update ``self.lastUpdate`` field with current date value."""
|
||||
self.lastUpdate = datetime.date.today()
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Return a field definition by numeric index or name string"""
|
||||
if isinstance(item, str):
|
||||
_name = item.upper()
|
||||
for _field in self.fields:
|
||||
if _field.name == _name:
|
||||
return _field
|
||||
else:
|
||||
raise KeyError(item)
|
||||
else:
|
||||
# item must be field index
|
||||
return self.fields[item]
|
||||
|
||||
# vim: et sts=4 sw=4 :
|
||||
@@ -0,0 +1,266 @@
|
||||
"""DBF record definition.
|
||||
|
||||
"""
|
||||
"""History (most recent first):
|
||||
11-feb-2007 [als] __repr__: added special case for invalid field values
|
||||
10-feb-2007 [als] added .rawFromStream()
|
||||
30-oct-2006 [als] fix record length in .fromStream()
|
||||
04-jul-2006 [als] added export declaration
|
||||
20-dec-2005 [yc] DbfRecord.write() -> DbfRecord._write();
|
||||
added delete() method.
|
||||
16-dec-2005 [yc] record definition moved from `dbf`.
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.7 $"[11:-2]
|
||||
__date__ = "$Date: 2007/02/11 09:05:49 $"[7:-2]
|
||||
|
||||
__all__ = ["DbfRecord"]
|
||||
|
||||
import sys
|
||||
|
||||
from . import utils
|
||||
|
||||
class DbfRecord(object):
|
||||
"""DBF record.
|
||||
|
||||
Instances of this class shouldn't be created manualy,
|
||||
use `dbf.Dbf.newRecord` instead.
|
||||
|
||||
Class implements mapping/sequence interface, so
|
||||
fields could be accessed via their names or indexes
|
||||
(names is a preffered way to access fields).
|
||||
|
||||
Hint:
|
||||
Use `store` method to save modified record.
|
||||
|
||||
Examples:
|
||||
Add new record to the database:
|
||||
db = Dbf(filename)
|
||||
rec = db.newRecord()
|
||||
rec["FIELD1"] = value1
|
||||
rec["FIELD2"] = value2
|
||||
rec.store()
|
||||
Or the same, but modify existed
|
||||
(second in this case) record:
|
||||
db = Dbf(filename)
|
||||
rec = db[2]
|
||||
rec["FIELD1"] = value1
|
||||
rec["FIELD2"] = value2
|
||||
rec.store()
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = "dbf", "index", "deleted", "fieldData"
|
||||
|
||||
## creation and initialization
|
||||
|
||||
def __init__(self, dbf, index=None, deleted=False, data=None):
|
||||
"""Instance initialiation.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance this record belonogs to.
|
||||
index:
|
||||
An integer record index or None. If this value is
|
||||
None, record will be appended to the DBF.
|
||||
deleted:
|
||||
Boolean flag indicating whether this record
|
||||
is a deleted record.
|
||||
data:
|
||||
A sequence or None. This is a data of the fields.
|
||||
If this argument is None, default values will be used.
|
||||
|
||||
"""
|
||||
self.dbf = dbf
|
||||
# XXX: I'm not sure ``index`` is necessary
|
||||
self.index = index
|
||||
self.deleted = deleted
|
||||
if data is None:
|
||||
self.fieldData = [_fd.defaultValue for _fd in dbf.header.fields]
|
||||
else:
|
||||
self.fieldData = list(data)
|
||||
|
||||
# XXX: validate self.index before calculating position?
|
||||
position = property(lambda self: self.dbf.header.headerLength + \
|
||||
self.index * self.dbf.header.recordLength)
|
||||
|
||||
def rawFromStream(cls, dbf, index):
|
||||
"""Return raw record contents read from the stream.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance containing the record.
|
||||
index:
|
||||
Index of the record in the records' container.
|
||||
This argument can't be None in this call.
|
||||
|
||||
Return value is a string containing record data in DBF format.
|
||||
|
||||
"""
|
||||
# XXX: may be write smth assuming, that current stream
|
||||
# position is the required one? it could save some
|
||||
# time required to calculate where to seek in the file
|
||||
dbf.stream.seek(dbf.header.headerLength +
|
||||
index * dbf.header.recordLength)
|
||||
return dbf.stream.read(dbf.header.recordLength)
|
||||
rawFromStream = classmethod(rawFromStream)
|
||||
|
||||
def fromStream(cls, dbf, index):
|
||||
"""Return a record read from the stream.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance new record should belong to.
|
||||
index:
|
||||
Index of the record in the records' container.
|
||||
This argument can't be None in this call.
|
||||
|
||||
Return value is an instance of the current class.
|
||||
|
||||
"""
|
||||
return cls.fromString(dbf, cls.rawFromStream(dbf, index), index)
|
||||
fromStream = classmethod(fromStream)
|
||||
|
||||
def fromString(cls, dbf, string, index=None):
|
||||
"""Return record read from the string object.
|
||||
|
||||
Arguments:
|
||||
dbf:
|
||||
A `Dbf.Dbf` instance new record should belong to.
|
||||
string:
|
||||
A string new record should be created from.
|
||||
index:
|
||||
Index of the record in the container. If this
|
||||
argument is None, record will be appended.
|
||||
|
||||
Return value is an instance of the current class.
|
||||
|
||||
"""
|
||||
return cls(dbf, index, string[0]=="*",
|
||||
[_fd.decodeFromRecord(string) for _fd in dbf.header.fields])
|
||||
fromString = classmethod(fromString)
|
||||
|
||||
## object representation
|
||||
|
||||
def __repr__(self):
|
||||
_template = "%%%ds: %%s (%%s)" % max([len(_fld)
|
||||
for _fld in self.dbf.fieldNames])
|
||||
_rv = []
|
||||
for _fld in self.dbf.fieldNames:
|
||||
_val = self[_fld]
|
||||
if _val is utils.INVALID_VALUE:
|
||||
_rv.append(_template %
|
||||
(_fld, "None", "value cannot be decoded"))
|
||||
else:
|
||||
_rv.append(_template % (_fld, _val, type(_val)))
|
||||
return "\n".join(_rv)
|
||||
|
||||
## protected methods
|
||||
|
||||
def _write(self):
|
||||
"""Write data to the dbf stream.
|
||||
|
||||
Note:
|
||||
This isn't a public method, it's better to
|
||||
use 'store' instead publically.
|
||||
Be design ``_write`` method should be called
|
||||
only from the `Dbf` instance.
|
||||
|
||||
|
||||
"""
|
||||
self._validateIndex(False)
|
||||
self.dbf.stream.seek(self.position)
|
||||
self.dbf.stream.write(bytes(self.toString(),
|
||||
sys.getfilesystemencoding()))
|
||||
# FIXME: may be move this write somewhere else?
|
||||
# why we should check this condition for each record?
|
||||
if self.index == len(self.dbf):
|
||||
# this is the last record,
|
||||
# we should write SUB (ASCII 26)
|
||||
self.dbf.stream.write(b"\x1A")
|
||||
|
||||
## utility methods
|
||||
|
||||
def _validateIndex(self, allowUndefined=True, checkRange=False):
|
||||
"""Valid ``self.index`` value.
|
||||
|
||||
If ``allowUndefined`` argument is True functions does nothing
|
||||
in case of ``self.index`` pointing to None object.
|
||||
|
||||
"""
|
||||
if self.index is None:
|
||||
if not allowUndefined:
|
||||
raise ValueError("Index is undefined")
|
||||
elif self.index < 0:
|
||||
raise ValueError("Index can't be negative (%s)" % self.index)
|
||||
elif checkRange and self.index <= self.dbf.header.recordCount:
|
||||
raise ValueError("There are only %d records in the DBF" %
|
||||
self.dbf.header.recordCount)
|
||||
|
||||
## interface methods
|
||||
|
||||
def store(self):
|
||||
"""Store current record in the DBF.
|
||||
|
||||
If ``self.index`` is None, this record will be appended to the
|
||||
records of the DBF this records belongs to; or replaced otherwise.
|
||||
|
||||
"""
|
||||
self._validateIndex()
|
||||
if self.index is None:
|
||||
self.index = len(self.dbf)
|
||||
self.dbf.append(self)
|
||||
else:
|
||||
self.dbf[self.index] = self
|
||||
|
||||
def delete(self):
|
||||
"""Mark method as deleted."""
|
||||
self.deleted = True
|
||||
|
||||
def toString(self):
|
||||
"""Return string packed record values."""
|
||||
# for (_def, _dat) in zip(self.dbf.header.fields, self.fieldData):
|
||||
#
|
||||
|
||||
return "".join([" *"[self.deleted]] + [
|
||||
_def.encodeValue(_dat)
|
||||
for (_def, _dat) in zip(self.dbf.header.fields, self.fieldData)
|
||||
])
|
||||
|
||||
def asList(self):
|
||||
"""Return a flat list of fields.
|
||||
|
||||
Note:
|
||||
Change of the list's values won't change
|
||||
real values stored in this object.
|
||||
|
||||
"""
|
||||
return self.fieldData[:]
|
||||
|
||||
def asDict(self):
|
||||
"""Return a dictionary of fields.
|
||||
|
||||
Note:
|
||||
Change of the dicts's values won't change
|
||||
real values stored in this object.
|
||||
|
||||
"""
|
||||
return dict([_i for _i in zip(self.dbf.fieldNames, self.fieldData)])
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Return value by field name or field index."""
|
||||
if isinstance(key, int):
|
||||
# integer index of the field
|
||||
return self.fieldData[key]
|
||||
# assuming string field name
|
||||
return self.fieldData[self.dbf.indexOfFieldName(key)]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set field value by integer index of the field or string name."""
|
||||
if isinstance(key, int):
|
||||
# integer index of the field
|
||||
return self.fieldData[key]
|
||||
# assuming string field name
|
||||
self.fieldData[self.dbf.indexOfFieldName(key)] = value
|
||||
|
||||
# vim: et sts=4 sw=4 :
|
||||
@@ -0,0 +1,170 @@
|
||||
"""String utilities.
|
||||
|
||||
TODO:
|
||||
- allow strings in getDateTime routine;
|
||||
"""
|
||||
"""History (most recent first):
|
||||
11-feb-2007 [als] added INVALID_VALUE
|
||||
10-feb-2007 [als] allow date strings padded with spaces instead of zeroes
|
||||
20-dec-2005 [yc] handle long objects in getDate/getDateTime
|
||||
16-dec-2005 [yc] created from ``strutil`` module.
|
||||
"""
|
||||
|
||||
__version__ = "$Revision: 1.4 $"[11:-2]
|
||||
__date__ = "$Date: 2007/02/11 08:57:17 $"[7:-2]
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
|
||||
def unzfill(str):
|
||||
"""Return a string without ASCII NULs.
|
||||
|
||||
This function searchers for the first NUL (ASCII 0) occurance
|
||||
and truncates string till that position.
|
||||
|
||||
"""
|
||||
try:
|
||||
return str[:str.index(b'\0')]
|
||||
except ValueError:
|
||||
return str
|
||||
|
||||
|
||||
def getDate(date=None):
|
||||
"""Return `datetime.date` instance.
|
||||
|
||||
Type of the ``date`` argument could be one of the following:
|
||||
None:
|
||||
use current date value;
|
||||
datetime.date:
|
||||
this value will be returned;
|
||||
datetime.datetime:
|
||||
the result of the date.date() will be returned;
|
||||
string:
|
||||
assuming "%Y%m%d" or "%y%m%dd" format;
|
||||
number:
|
||||
assuming it's a timestamp (returned for example
|
||||
by the time.time() call;
|
||||
sequence:
|
||||
assuming (year, month, day, ...) sequence;
|
||||
|
||||
Additionaly, if ``date`` has callable ``ticks`` attribute,
|
||||
it will be used and result of the called would be treated
|
||||
as a timestamp value.
|
||||
|
||||
"""
|
||||
if date is None:
|
||||
# use current value
|
||||
return datetime.date.today()
|
||||
if isinstance(date, datetime.date):
|
||||
return date
|
||||
if isinstance(date, datetime.datetime):
|
||||
return date.date()
|
||||
if isinstance(date, (int, float)):
|
||||
# date is a timestamp
|
||||
return datetime.date.fromtimestamp(date)
|
||||
if isinstance(date, str):
|
||||
date = date.replace(" ", "0")
|
||||
if len(date) == 6:
|
||||
# yymmdd
|
||||
return datetime.date(*time.strptime(date, "%y%m%d")[:3])
|
||||
# yyyymmdd
|
||||
return datetime.date(*time.strptime(date, "%Y%m%d")[:3])
|
||||
if hasattr(date, "__getitem__"):
|
||||
# a sequence (assuming date/time tuple)
|
||||
return datetime.date(*date[:3])
|
||||
return datetime.date.fromtimestamp(date.ticks())
|
||||
|
||||
|
||||
def getDateTime(value=None):
|
||||
"""Return `datetime.datetime` instance.
|
||||
|
||||
Type of the ``value`` argument could be one of the following:
|
||||
None:
|
||||
use current date value;
|
||||
datetime.date:
|
||||
result will be converted to the `datetime.datetime` instance
|
||||
using midnight;
|
||||
datetime.datetime:
|
||||
``value`` will be returned as is;
|
||||
string:
|
||||
*** CURRENTLY NOT SUPPORTED ***;
|
||||
number:
|
||||
assuming it's a timestamp (returned for example
|
||||
by the time.time() call;
|
||||
sequence:
|
||||
assuming (year, month, day, ...) sequence;
|
||||
|
||||
Additionaly, if ``value`` has callable ``ticks`` attribute,
|
||||
it will be used and result of the called would be treated
|
||||
as a timestamp value.
|
||||
|
||||
"""
|
||||
if value is None:
|
||||
# use current value
|
||||
return datetime.datetime.today()
|
||||
if isinstance(value, datetime.datetime):
|
||||
return value
|
||||
if isinstance(value, datetime.date):
|
||||
return datetime.datetime.fromordinal(value.toordinal())
|
||||
if isinstance(value, (int, float)):
|
||||
# value is a timestamp
|
||||
return datetime.datetime.fromtimestamp(value)
|
||||
if isinstance(value, str):
|
||||
raise NotImplementedError("Strings aren't currently implemented")
|
||||
if hasattr(value, "__getitem__"):
|
||||
# a sequence (assuming date/time tuple)
|
||||
return datetime.datetime(*tuple(value)[:6])
|
||||
return datetime.datetime.fromtimestamp(value.ticks())
|
||||
|
||||
|
||||
class classproperty(property):
|
||||
"""Works in the same way as a ``property``, but for the classes."""
|
||||
|
||||
def __get__(self, obj, cls):
|
||||
return self.fget(cls)
|
||||
|
||||
|
||||
class _InvalidValue(object):
|
||||
|
||||
"""Value returned from DBF records when field validation fails
|
||||
|
||||
The value is not equal to anything except for itself
|
||||
and equal to all empty values: None, 0, empty string etc.
|
||||
In other words, invalid value is equal to None and not equal
|
||||
to None at the same time.
|
||||
|
||||
This value yields zero upon explicit conversion to a number type,
|
||||
empty string for string types, and False for boolean.
|
||||
|
||||
"""
|
||||
|
||||
def __eq__(self, other):
|
||||
return not other
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (other is self)
|
||||
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __int__(self):
|
||||
return 0
|
||||
__long__ = __int__
|
||||
|
||||
def __float__(self):
|
||||
return 0.0
|
||||
|
||||
def __str__(self):
|
||||
return ""
|
||||
|
||||
def __unicode__(self):
|
||||
return ""
|
||||
|
||||
def __repr__(self):
|
||||
return "<INVALID>"
|
||||
|
||||
# invalid value is a constant singleton
|
||||
INVALID_VALUE = _InvalidValue()
|
||||
|
||||
# vim: set et sts=4 sw=4 :
|
||||
+32
-34
@@ -33,7 +33,7 @@ class element:
|
||||
self.tag = tag.lower( )
|
||||
else:
|
||||
self.tag = tag.upper( )
|
||||
|
||||
|
||||
def __call__( self, *args, **kwargs ):
|
||||
if len( args ) > 1:
|
||||
raise ArgumentError( self.tag )
|
||||
@@ -42,14 +42,14 @@ class element:
|
||||
if self.parent is not None and self.parent.class_ is not None:
|
||||
if 'class_' not in kwargs:
|
||||
kwargs['class_'] = self.parent.class_
|
||||
|
||||
|
||||
if self.parent is None and len( args ) == 1:
|
||||
x = [ self.render( self.tag, False, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
|
||||
return '\n'.join( x )
|
||||
elif self.parent is None and len( args ) == 0:
|
||||
x = [ self.render( self.tag, True, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
|
||||
return '\n'.join( x )
|
||||
|
||||
|
||||
if self.tag in self.parent.twotags:
|
||||
for myarg, mydict in _argsdicts( args, kwargs ):
|
||||
self.render( self.tag, False, myarg, mydict )
|
||||
@@ -63,33 +63,31 @@ class element:
|
||||
raise DeprecationError( self.tag )
|
||||
else:
|
||||
raise InvalidElementError( self.tag, self.parent.mode )
|
||||
|
||||
|
||||
def render( self, tag, single, between, kwargs ):
|
||||
"""Append the actual tags to content."""
|
||||
|
||||
out = "<%s" % tag
|
||||
out = u"<%s" % tag
|
||||
for key, value in kwargs.iteritems( ):
|
||||
if value is not None: # when value is None that means stuff like <... checked>
|
||||
key = key.strip('_') # strip this so class_ will mean class, etc.
|
||||
if key == 'http_equiv': # special cases, maybe change _ to - overall?
|
||||
key = 'http-equiv'
|
||||
elif key == 'accept_charset':
|
||||
key = 'accept-charset'
|
||||
out = "%s %s=\"%s\"" % ( out, key, escape( value ) )
|
||||
if key in ['http_equiv', 'accept_charset']:
|
||||
key.replace('_','-')
|
||||
out = u"%s %s=\"%s\"" % ( out, key, escape( value ) )
|
||||
else:
|
||||
out = "%s %s" % ( out, key )
|
||||
out = u"%s %s" % ( out, key )
|
||||
if between is not None:
|
||||
out = "%s>%s</%s>" % ( out, between, tag )
|
||||
out = u"%s>%s</%s>" % ( out, between, tag )
|
||||
else:
|
||||
if single:
|
||||
out = "%s />" % out
|
||||
out = u"%s />" % out
|
||||
else:
|
||||
out = "%s>" % out
|
||||
out = u"%s>" % out
|
||||
if self.parent is not None:
|
||||
self.parent.content.append( out )
|
||||
else:
|
||||
return out
|
||||
|
||||
|
||||
def close( self ):
|
||||
"""Append a closing tag unless element has only opening tag."""
|
||||
|
||||
@@ -128,11 +126,11 @@ class page:
|
||||
these two keyword arguments may be used to select
|
||||
the set of valid elements in 'xml' mode
|
||||
invalid elements will raise appropriate exceptions
|
||||
|
||||
|
||||
separator -- string to place between added elements, defaults to newline
|
||||
|
||||
|
||||
class_ -- a class that will be added to every element if defined"""
|
||||
|
||||
|
||||
valid_onetags = [ "AREA", "BASE", "BR", "COL", "FRAME", "HR", "IMG", "INPUT", "LINK", "META", "PARAM" ]
|
||||
valid_twotags = [ "A", "ABBR", "ACRONYM", "ADDRESS", "B", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BUTTON",
|
||||
"CAPTION", "CITE", "CODE", "COLGROUP", "DD", "DEL", "DFN", "DIV", "DL", "DT", "EM", "FIELDSET",
|
||||
@@ -163,7 +161,7 @@ class page:
|
||||
self.deptags += map( string.lower, self.deptags )
|
||||
self.mode = 'strict_html'
|
||||
elif mode == 'loose_html':
|
||||
self.onetags = valid_onetags + deprecated_onetags
|
||||
self.onetags = valid_onetags + deprecated_onetags
|
||||
self.onetags += map( string.lower, self.onetags )
|
||||
self.twotags = valid_twotags + deprecated_twotags
|
||||
self.twotags += map( string.lower, self.twotags )
|
||||
@@ -183,16 +181,16 @@ class page:
|
||||
|
||||
def __getattr__( self, attr ):
|
||||
if attr.startswith("__") and attr.endswith("__"):
|
||||
raise AttributeError, attr
|
||||
raise AttributeError(attr)
|
||||
return element( attr, case=self.case, parent=self )
|
||||
|
||||
def __str__( self ):
|
||||
|
||||
|
||||
if self._full and ( self.mode == 'strict_html' or self.mode == 'loose_html' ):
|
||||
end = [ '</body>', '</html>' ]
|
||||
else:
|
||||
end = [ ]
|
||||
|
||||
|
||||
return self.separator.join( self.header + self.content + self.footer + end )
|
||||
|
||||
def __call__( self, escape=False ):
|
||||
@@ -232,7 +230,7 @@ class page:
|
||||
|
||||
lang -- language, usually a two character string, will appear
|
||||
as <html lang='en'> in html mode (ignored in xml mode)
|
||||
|
||||
|
||||
css -- Cascading Style Sheet filename as a string or a list of
|
||||
strings for multiple css files (ignored in xml mode)
|
||||
|
||||
@@ -306,7 +304,7 @@ class page:
|
||||
def css( self, filelist ):
|
||||
"""This convenience function is only useful for html.
|
||||
It adds css stylesheet(s) to the document via the <link> element."""
|
||||
|
||||
|
||||
if isinstance( filelist, basestring ):
|
||||
self.link( href=filelist, rel='stylesheet', type='text/css', media='all' )
|
||||
else:
|
||||
@@ -322,7 +320,7 @@ class page:
|
||||
for name, content in mydict.iteritems( ):
|
||||
self.meta( name=name, content=content )
|
||||
else:
|
||||
raise TypeError, "Metainfo should be called with a dictionary argument of name:content pairs."
|
||||
raise TypeError ("Metainfo should be called with a dictionary argument of name:content pairs.")
|
||||
|
||||
def scripts( self, mydict ):
|
||||
"""Only useful in html, mydict is dictionary of src:type pairs will
|
||||
@@ -332,20 +330,20 @@ class page:
|
||||
for src, type in mydict.iteritems( ):
|
||||
self.script( '', src=src, type='text/%s' % type )
|
||||
else:
|
||||
raise TypeError, "Script should be given a dictionary of src:type pairs."
|
||||
raise TypeError ("Script should be given a dictionary of src:type pairs.")
|
||||
|
||||
|
||||
class _oneliner:
|
||||
"""An instance of oneliner returns a string corresponding to one element.
|
||||
This class can be used to write 'oneliners' that return a string
|
||||
immediately so there is no need to instantiate the page class."""
|
||||
|
||||
|
||||
def __init__( self, case='lower' ):
|
||||
self.case = case
|
||||
|
||||
|
||||
def __getattr__( self, attr ):
|
||||
if attr.startswith("__") and attr.endswith("__"):
|
||||
raise AttributeError, attr
|
||||
raise AttributeError(attr)
|
||||
return element( attr, case=self.case, parent=None )
|
||||
|
||||
oneliner = _oneliner( case='lower' )
|
||||
@@ -353,13 +351,13 @@ upper_oneliner = _oneliner( case='upper' )
|
||||
|
||||
def _argsdicts( args, mydict ):
|
||||
"""A utility generator that pads argument list and dictionary values, will only be called with len( args ) = 0, 1."""
|
||||
|
||||
|
||||
if len( args ) == 0:
|
||||
args = None,
|
||||
args = None,
|
||||
elif len( args ) == 1:
|
||||
args = _totuple( args[0] )
|
||||
else:
|
||||
raise Exception, "We should have never gotten here."
|
||||
raise Exception("We should have never gotten here.")
|
||||
|
||||
mykeys = mydict.keys( )
|
||||
myvalues = map( _totuple, mydict.values( ) )
|
||||
@@ -418,7 +416,7 @@ _escape = escape
|
||||
|
||||
def unescape( text ):
|
||||
"""Inverse of escape."""
|
||||
|
||||
|
||||
if isinstance( text, basestring ):
|
||||
if '&' in text:
|
||||
text = text.replace( '&', '&' )
|
||||
@@ -481,4 +479,4 @@ class CustomizationError( MarkupError ):
|
||||
self.message = "If you customize the allowed elements, you must define both types 'onetags' and 'twotags'."
|
||||
|
||||
if __name__ == '__main__':
|
||||
print __doc__
|
||||
print (__doc__)
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# 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',)
|
||||
@@ -1,384 +0,0 @@
|
||||
# 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)))
|
||||
@@ -1,340 +0,0 @@
|
||||
'''
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
'''
|
||||
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))
|
||||
@@ -1,68 +0,0 @@
|
||||
# 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
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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
|
||||
@@ -1,109 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,348 +0,0 @@
|
||||
# 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
|
||||
@@ -1,64 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,69 +0,0 @@
|
||||
# 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
|
||||
@@ -1,156 +0,0 @@
|
||||
# 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
|
||||
@@ -1,114 +0,0 @@
|
||||
# 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
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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
|
||||
@@ -1,154 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,59 +0,0 @@
|
||||
# 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"""
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# 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'
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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:]
|
||||
@@ -1,67 +0,0 @@
|
||||
# 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
|
||||
@@ -1,114 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,392 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,186 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,534 +0,0 @@
|
||||
# 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)
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# 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
|
||||
@@ -1,261 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,192 +0,0 @@
|
||||
# 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'})
|
||||
@@ -1,256 +0,0 @@
|
||||
# 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 []
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
# 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
|
||||
@@ -1,86 +0,0 @@
|
||||
# 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
|
||||
@@ -1,256 +0,0 @@
|
||||
# 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
|
||||
@@ -1,202 +0,0 @@
|
||||
# -*- 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)
|
||||
@@ -1,204 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,209 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,53 +0,0 @@
|
||||
# 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',)
|
||||
@@ -1,384 +0,0 @@
|
||||
# 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)))
|
||||
@@ -1,340 +0,0 @@
|
||||
'''
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
'''
|
||||
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))
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# 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
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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
|
||||
@@ -1,117 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,343 +0,0 @@
|
||||
# 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
|
||||
@@ -1,64 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,69 +0,0 @@
|
||||
# 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
|
||||
@@ -1,156 +0,0 @@
|
||||
# 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
|
||||
@@ -1,117 +0,0 @@
|
||||
# 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
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user