agate-excel 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agate_excel-0.4.2.dist-info/METADATA +78 -0
- agate_excel-0.4.2.dist-info/RECORD +9 -0
- agate_excel-0.4.2.dist-info/WHEEL +5 -0
- agate_excel-0.4.2.dist-info/licenses/AUTHORS.rst +10 -0
- agate_excel-0.4.2.dist-info/licenses/COPYING +21 -0
- agate_excel-0.4.2.dist-info/top_level.txt +1 -0
- agateexcel/__init__.py +2 -0
- agateexcel/table_xls.py +208 -0
- agateexcel/table_xlsx.py +154 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agate-excel
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: agate-excel adds read support for Excel files (xls and xlsx) to agate.
|
|
5
|
+
Home-page: https://agate-excel.readthedocs.org/
|
|
6
|
+
Author: Christopher Groskopf
|
|
7
|
+
Author-email: chrisgroskopf@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Description-Content-Type: text/x-rst
|
|
26
|
+
License-File: COPYING
|
|
27
|
+
License-File: AUTHORS.rst
|
|
28
|
+
Requires-Dist: agate>=1.5.0
|
|
29
|
+
Requires-Dist: olefile
|
|
30
|
+
Requires-Dist: openpyxl>=2.3.0
|
|
31
|
+
Requires-Dist: xlrd>=0.9.4
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
35
|
+
Dynamic: author
|
|
36
|
+
Dynamic: author-email
|
|
37
|
+
Dynamic: classifier
|
|
38
|
+
Dynamic: description
|
|
39
|
+
Dynamic: description-content-type
|
|
40
|
+
Dynamic: home-page
|
|
41
|
+
Dynamic: license
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
Dynamic: provides-extra
|
|
44
|
+
Dynamic: requires-dist
|
|
45
|
+
Dynamic: summary
|
|
46
|
+
|
|
47
|
+
.. image:: https://github.com/wireservice/agate-excel/workflows/CI/badge.svg
|
|
48
|
+
:target: https://github.com/wireservice/agate-excel/actions
|
|
49
|
+
:alt: Build status
|
|
50
|
+
|
|
51
|
+
.. image:: https://coveralls.io/repos/wireservice/agate-excel/badge.svg?branch=master
|
|
52
|
+
:target: https://coveralls.io/r/wireservice/agate-excel
|
|
53
|
+
:alt: Coverage status
|
|
54
|
+
|
|
55
|
+
.. image:: https://img.shields.io/pypi/dm/agate-excel.svg
|
|
56
|
+
:target: https://pypi.python.org/pypi/agate-excel
|
|
57
|
+
:alt: PyPI downloads
|
|
58
|
+
|
|
59
|
+
.. image:: https://img.shields.io/pypi/v/agate-excel.svg
|
|
60
|
+
:target: https://pypi.python.org/pypi/agate-excel
|
|
61
|
+
:alt: Version
|
|
62
|
+
|
|
63
|
+
.. image:: https://img.shields.io/pypi/l/agate-excel.svg
|
|
64
|
+
:target: https://pypi.python.org/pypi/agate-excel
|
|
65
|
+
:alt: License
|
|
66
|
+
|
|
67
|
+
.. image:: https://img.shields.io/pypi/pyversions/agate-excel.svg
|
|
68
|
+
:target: https://pypi.python.org/pypi/agate-excel
|
|
69
|
+
:alt: Support Python versions
|
|
70
|
+
|
|
71
|
+
agate-excel adds read support for Excel files (xls and xlsx) to `agate <https://github.com/wireservice/agate>`_.
|
|
72
|
+
|
|
73
|
+
Important links:
|
|
74
|
+
|
|
75
|
+
* agate https://agate.rtfd.org
|
|
76
|
+
* Documentation: https://agate-excel.rtfd.org
|
|
77
|
+
* Repository: https://github.com/wireservice/agate-excel
|
|
78
|
+
* Issues: https://github.com/wireservice/agate-excel/issues
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
agate_excel-0.4.2.dist-info/licenses/AUTHORS.rst,sha256=LxslP-WPSc4X-1SVMay7LpF1aV-1gMKkQhw7nQRcPR4,472
|
|
2
|
+
agate_excel-0.4.2.dist-info/licenses/COPYING,sha256=bndZjP9C4DkH4eexbIqZb8tRUKF8VuzSsAyqpFWJ-pw,1099
|
|
3
|
+
agateexcel/__init__.py,sha256=OLWNy-2mBZTFhB9kq-vszTIX5mhh9OrgktUmLqVhG60,57
|
|
4
|
+
agateexcel/table_xls.py,sha256=txVc33fjx7kXqORmTWWZkS4bVBM7axt2zjbzzrWTerI,6394
|
|
5
|
+
agateexcel/table_xlsx.py,sha256=PMTn5JnOHgZw3wTgPRCDvYXdtgmfiTn5ViJKcN9VzFg,4624
|
|
6
|
+
agate_excel-0.4.2.dist-info/METADATA,sha256=ev200UMQTPqpSKN_RjXe1Yalk9Sp64rOLL6CjRufgq8,2899
|
|
7
|
+
agate_excel-0.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
agate_excel-0.4.2.dist-info/top_level.txt,sha256=gDSv0yVz_5r9fBxWddsHzVojk1O11SfaayzkgOuFy_s,11
|
|
9
|
+
agate_excel-0.4.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
The following individuals have contributed code to agate-excel:
|
|
2
|
+
|
|
3
|
+
* `Christopher Groskopf <https://github.com/onyxfish>`_
|
|
4
|
+
* `Ben Welsh <https://github.com/palewire>`_
|
|
5
|
+
* `James McKinney <https://github.com/jpmckinney>`_
|
|
6
|
+
* `Peter M. Landwehr <https://github.com/pmlandwehr>`_
|
|
7
|
+
* `Jani Mikkonen <https://github.com/rasjani>`_
|
|
8
|
+
* `Tim Freund <https://github.com/timfreund>`_
|
|
9
|
+
* `Loïc Corbasson <https://github.com/lcorbasson>`_
|
|
10
|
+
* `Robert Schütz <https://github.com/dotlambda>`_
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Christopher Groskopf and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agateexcel
|
agateexcel/__init__.py
ADDED
agateexcel/table_xls.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the XLS extension to :class:`Table <agate.table.Table>`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
|
|
8
|
+
import agate
|
|
9
|
+
import olefile
|
|
10
|
+
import xlrd
|
|
11
|
+
|
|
12
|
+
EXCEL_TO_AGATE_TYPE = {
|
|
13
|
+
xlrd.biffh.XL_CELL_EMPTY: agate.Boolean(),
|
|
14
|
+
xlrd.biffh.XL_CELL_TEXT: agate.Text(),
|
|
15
|
+
xlrd.biffh.XL_CELL_NUMBER: agate.Number(),
|
|
16
|
+
xlrd.biffh.XL_CELL_DATE: agate.DateTime(),
|
|
17
|
+
xlrd.biffh.XL_CELL_BOOLEAN: agate.Boolean(),
|
|
18
|
+
xlrd.biffh.XL_CELL_ERROR: agate.Text(),
|
|
19
|
+
xlrd.biffh.XL_CELL_BLANK: agate.Boolean(),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def from_xls(cls, path, sheet=None, skip_lines=0, header=True, encoding_override=None, row_limit=None, **kwargs):
|
|
24
|
+
"""
|
|
25
|
+
Parse an XLS file.
|
|
26
|
+
|
|
27
|
+
:param path:
|
|
28
|
+
Path to an XLS file to load or a file-like object for one.
|
|
29
|
+
:param sheet:
|
|
30
|
+
The names or integer indices of the worksheets to load. If not specified
|
|
31
|
+
then the first sheet will be used.
|
|
32
|
+
:param skip_lines:
|
|
33
|
+
The number of rows to skip from the top of the sheet.
|
|
34
|
+
:param header:
|
|
35
|
+
If :code:`True`, the first row is assumed to contain column names.
|
|
36
|
+
:param row_limit:
|
|
37
|
+
Limit how many rows of data will be read.
|
|
38
|
+
"""
|
|
39
|
+
if not isinstance(skip_lines, int):
|
|
40
|
+
raise ValueError('skip_lines argument must be an int')
|
|
41
|
+
|
|
42
|
+
def open_workbook(f):
|
|
43
|
+
try:
|
|
44
|
+
book = xlrd.open_workbook(file_contents=f.read(), encoding_override=encoding_override, on_demand=True)
|
|
45
|
+
except xlrd.compdoc.CompDocError:
|
|
46
|
+
# This is not a pure XLS file; we'll try to read it though.
|
|
47
|
+
# Let's try the Compound File Binary Format:
|
|
48
|
+
ole = olefile.OleFileIO(f)
|
|
49
|
+
if ole.exists('Workbook'):
|
|
50
|
+
d = ole.openstream('Workbook')
|
|
51
|
+
book = xlrd.open_workbook(file_contents=d.read(), on_demand=True)
|
|
52
|
+
else:
|
|
53
|
+
raise OSError('No Workbook stream found in OLE file')
|
|
54
|
+
return book
|
|
55
|
+
|
|
56
|
+
if hasattr(path, 'read'):
|
|
57
|
+
book = open_workbook(path)
|
|
58
|
+
else:
|
|
59
|
+
with open(path, 'rb') as f:
|
|
60
|
+
book = open_workbook(f)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
multiple = agate.utils.issequence(sheet)
|
|
64
|
+
if multiple:
|
|
65
|
+
sheets = sheet
|
|
66
|
+
else:
|
|
67
|
+
sheets = [sheet]
|
|
68
|
+
|
|
69
|
+
tables = OrderedDict()
|
|
70
|
+
|
|
71
|
+
for i, sheet in enumerate(sheets):
|
|
72
|
+
if isinstance(sheet, str):
|
|
73
|
+
sheet = book.sheet_by_name(sheet)
|
|
74
|
+
elif isinstance(sheet, int):
|
|
75
|
+
sheet = book.sheet_by_index(sheet)
|
|
76
|
+
else:
|
|
77
|
+
sheet = book.sheet_by_index(0)
|
|
78
|
+
|
|
79
|
+
if header:
|
|
80
|
+
offset = 1
|
|
81
|
+
column_names = []
|
|
82
|
+
else:
|
|
83
|
+
offset = 0
|
|
84
|
+
column_names = None
|
|
85
|
+
|
|
86
|
+
columns = []
|
|
87
|
+
column_types = []
|
|
88
|
+
|
|
89
|
+
for i in range(sheet.ncols):
|
|
90
|
+
if row_limit is None:
|
|
91
|
+
values = sheet.col_values(i, skip_lines + offset)
|
|
92
|
+
types = sheet.col_types(i, skip_lines + offset)
|
|
93
|
+
else:
|
|
94
|
+
values = sheet.col_values(i, skip_lines + offset, skip_lines + offset + row_limit)
|
|
95
|
+
types = sheet.col_types(i, skip_lines + offset, skip_lines + offset + row_limit)
|
|
96
|
+
excel_type = determine_excel_type(types)
|
|
97
|
+
agate_type = determine_agate_type(excel_type)
|
|
98
|
+
|
|
99
|
+
if excel_type == xlrd.biffh.XL_CELL_BOOLEAN:
|
|
100
|
+
values = normalize_booleans(values)
|
|
101
|
+
elif excel_type == xlrd.biffh.XL_CELL_DATE:
|
|
102
|
+
values, with_date, with_time = normalize_dates(values, book.datemode)
|
|
103
|
+
if not with_date:
|
|
104
|
+
agate_type = agate.TimeDelta()
|
|
105
|
+
if not with_time:
|
|
106
|
+
agate_type = agate.Date()
|
|
107
|
+
|
|
108
|
+
if header:
|
|
109
|
+
name = str(sheet.cell_value(skip_lines, i)) or None
|
|
110
|
+
column_names.append(name)
|
|
111
|
+
|
|
112
|
+
columns.append(values)
|
|
113
|
+
column_types.append(agate_type)
|
|
114
|
+
|
|
115
|
+
rows = []
|
|
116
|
+
|
|
117
|
+
if columns:
|
|
118
|
+
for i in range(len(columns[0])):
|
|
119
|
+
rows.append([c[i] for c in columns])
|
|
120
|
+
|
|
121
|
+
if 'column_names' in kwargs:
|
|
122
|
+
if not header:
|
|
123
|
+
column_names = kwargs['column_names']
|
|
124
|
+
del kwargs['column_names']
|
|
125
|
+
|
|
126
|
+
if 'column_types' in kwargs:
|
|
127
|
+
column_types = kwargs['column_types']
|
|
128
|
+
del kwargs['column_types']
|
|
129
|
+
|
|
130
|
+
tables[sheet.name] = agate.Table(rows, column_names, column_types, **kwargs)
|
|
131
|
+
|
|
132
|
+
finally:
|
|
133
|
+
book.release_resources()
|
|
134
|
+
|
|
135
|
+
if multiple:
|
|
136
|
+
return agate.MappedSequence(tables.values(), tables.keys())
|
|
137
|
+
return tables.popitem()[1]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def determine_agate_type(excel_type):
|
|
141
|
+
try:
|
|
142
|
+
return EXCEL_TO_AGATE_TYPE[excel_type]
|
|
143
|
+
except KeyError:
|
|
144
|
+
return agate.Text()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def determine_excel_type(types):
|
|
148
|
+
"""
|
|
149
|
+
Determine the correct type for a column from a list of cell types.
|
|
150
|
+
"""
|
|
151
|
+
types_set = set(types)
|
|
152
|
+
types_set.discard(xlrd.biffh.XL_CELL_EMPTY)
|
|
153
|
+
|
|
154
|
+
# Normalize mixed types to text
|
|
155
|
+
if len(types_set) > 1:
|
|
156
|
+
return xlrd.biffh.XL_CELL_TEXT
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
return types_set.pop()
|
|
160
|
+
except KeyError:
|
|
161
|
+
return xlrd.biffh.XL_CELL_EMPTY
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def normalize_booleans(values):
|
|
165
|
+
normalized = []
|
|
166
|
+
|
|
167
|
+
for value in values:
|
|
168
|
+
if value is None or value == '':
|
|
169
|
+
normalized.append(None)
|
|
170
|
+
else:
|
|
171
|
+
normalized.append(bool(value))
|
|
172
|
+
|
|
173
|
+
return normalized
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def normalize_dates(values, datemode=0):
|
|
177
|
+
"""
|
|
178
|
+
Normalize a column of date cells.
|
|
179
|
+
"""
|
|
180
|
+
normalized = []
|
|
181
|
+
with_date = False
|
|
182
|
+
with_time = False
|
|
183
|
+
|
|
184
|
+
for v in values:
|
|
185
|
+
if not v:
|
|
186
|
+
normalized.append(None)
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
v_tuple = xlrd.xldate.xldate_as_datetime(v, datemode).timetuple()
|
|
190
|
+
|
|
191
|
+
if v_tuple[3:6] == (0, 0, 0):
|
|
192
|
+
# Date only
|
|
193
|
+
normalized.append(datetime.date(*v_tuple[:3]))
|
|
194
|
+
with_date = True
|
|
195
|
+
elif v_tuple[:3] == (0, 0, 0):
|
|
196
|
+
# Time only
|
|
197
|
+
normalized.append(datetime.time(*v_tuple[3:6]))
|
|
198
|
+
with_time = True
|
|
199
|
+
else:
|
|
200
|
+
# Date and time
|
|
201
|
+
normalized.append(datetime.datetime(*v_tuple[:6]))
|
|
202
|
+
with_date = True
|
|
203
|
+
with_time = True
|
|
204
|
+
|
|
205
|
+
return (normalized, with_date, with_time)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
agate.Table.from_xls = classmethod(from_xls)
|
agateexcel/table_xlsx.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the XLSX extension to :class:`Table <agate.table.Table>`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
|
|
8
|
+
import agate
|
|
9
|
+
import openpyxl
|
|
10
|
+
|
|
11
|
+
NULL_TIME = datetime.time(0, 0, 0)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def from_xlsx(cls, path, sheet=None, skip_lines=0, header=True, read_only=True,
|
|
15
|
+
reset_dimensions=None, row_limit=None, **kwargs):
|
|
16
|
+
"""
|
|
17
|
+
Parse an XLSX file.
|
|
18
|
+
|
|
19
|
+
:param path:
|
|
20
|
+
Path to an XLSX file to load or a file-like object for one.
|
|
21
|
+
:param sheet:
|
|
22
|
+
The names or integer indices of the worksheets to load. If not specified
|
|
23
|
+
then the "active" sheet will be used.
|
|
24
|
+
:param skip_lines:
|
|
25
|
+
The number of rows to skip from the top of the sheet.
|
|
26
|
+
:param header:
|
|
27
|
+
If :code:`True`, the first row is assumed to contain column names.
|
|
28
|
+
:param read_only:
|
|
29
|
+
If :code:`True`, the XLSX file is opened in read-only mode, to reduce
|
|
30
|
+
memory consumption.
|
|
31
|
+
:param reset_dimensions:
|
|
32
|
+
If :code:`True`, do not trust the dimensions in the file's properties,
|
|
33
|
+
and recalculate them based on the data in the file.
|
|
34
|
+
:param row_limit:
|
|
35
|
+
Limit how many rows of data will be read.
|
|
36
|
+
"""
|
|
37
|
+
if not isinstance(skip_lines, int):
|
|
38
|
+
raise ValueError('skip_lines argument must be an int')
|
|
39
|
+
|
|
40
|
+
if hasattr(path, 'read'):
|
|
41
|
+
f = path
|
|
42
|
+
else:
|
|
43
|
+
f = open(path, 'rb')
|
|
44
|
+
|
|
45
|
+
book = openpyxl.load_workbook(f, read_only=read_only, data_only=True)
|
|
46
|
+
|
|
47
|
+
multiple = agate.utils.issequence(sheet)
|
|
48
|
+
if multiple:
|
|
49
|
+
sheets = sheet
|
|
50
|
+
else:
|
|
51
|
+
sheets = [sheet]
|
|
52
|
+
|
|
53
|
+
tables = OrderedDict()
|
|
54
|
+
|
|
55
|
+
for i, sheet in enumerate(sheets):
|
|
56
|
+
if isinstance(sheet, str):
|
|
57
|
+
try:
|
|
58
|
+
sheet = book[sheet]
|
|
59
|
+
except KeyError:
|
|
60
|
+
f.close()
|
|
61
|
+
raise
|
|
62
|
+
elif isinstance(sheet, int):
|
|
63
|
+
try:
|
|
64
|
+
sheet = book.worksheets[sheet]
|
|
65
|
+
except IndexError:
|
|
66
|
+
f.close()
|
|
67
|
+
raise
|
|
68
|
+
else:
|
|
69
|
+
sheet = book.active
|
|
70
|
+
|
|
71
|
+
column_names = None
|
|
72
|
+
offset = 0
|
|
73
|
+
rows = []
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
read_only
|
|
77
|
+
and (reset_dimensions or (reset_dimensions is None and sheet.max_column == 1 and sheet.max_row == 1))
|
|
78
|
+
):
|
|
79
|
+
try:
|
|
80
|
+
sheet.reset_dimensions()
|
|
81
|
+
sheet.calculate_dimension(force=True)
|
|
82
|
+
# https://foss.heptapod.net/openpyxl/openpyxl/-/issues/2111
|
|
83
|
+
except UnboundLocalError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
if header:
|
|
87
|
+
sheet_header = sheet.iter_rows(min_row=1 + skip_lines, max_row=1 + skip_lines)
|
|
88
|
+
column_names = [None if c.value is None else str(c.value) for row in sheet_header for c in row]
|
|
89
|
+
offset = 1
|
|
90
|
+
|
|
91
|
+
if row_limit is None:
|
|
92
|
+
sheet_rows = sheet.iter_rows(min_row=1 + skip_lines + offset)
|
|
93
|
+
else:
|
|
94
|
+
sheet_rows = sheet.iter_rows(min_row=1 + skip_lines + offset, max_row=1 + skip_lines + offset + row_limit)
|
|
95
|
+
|
|
96
|
+
for i, row in enumerate(sheet_rows):
|
|
97
|
+
values = []
|
|
98
|
+
|
|
99
|
+
for c in row:
|
|
100
|
+
value = c.value
|
|
101
|
+
|
|
102
|
+
if value.__class__ is datetime.datetime:
|
|
103
|
+
# Handle default XLSX date as 00:00 time
|
|
104
|
+
if value.date() == datetime.date(1904, 1, 1) and not has_date_elements(c):
|
|
105
|
+
value = value.time()
|
|
106
|
+
|
|
107
|
+
value = normalize_datetime(value)
|
|
108
|
+
elif value.time() == NULL_TIME:
|
|
109
|
+
value = value.date()
|
|
110
|
+
else:
|
|
111
|
+
value = normalize_datetime(value)
|
|
112
|
+
|
|
113
|
+
values.append(value)
|
|
114
|
+
|
|
115
|
+
rows.append(values)
|
|
116
|
+
|
|
117
|
+
if 'column_names' in kwargs:
|
|
118
|
+
if not header:
|
|
119
|
+
column_names = kwargs['column_names']
|
|
120
|
+
del kwargs['column_names']
|
|
121
|
+
|
|
122
|
+
tables[sheet.title] = agate.Table(rows, column_names, **kwargs)
|
|
123
|
+
|
|
124
|
+
f.close()
|
|
125
|
+
|
|
126
|
+
if multiple:
|
|
127
|
+
return agate.MappedSequence(tables.values(), tables.keys())
|
|
128
|
+
return tables.popitem()[1]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def normalize_datetime(dt):
|
|
132
|
+
if dt.microsecond == 0:
|
|
133
|
+
return dt
|
|
134
|
+
|
|
135
|
+
ms = dt.microsecond
|
|
136
|
+
|
|
137
|
+
if ms < 1000:
|
|
138
|
+
return dt.replace(microsecond=0)
|
|
139
|
+
if ms > 999000:
|
|
140
|
+
return dt.replace(microsecond=0) + datetime.timedelta(seconds=1)
|
|
141
|
+
|
|
142
|
+
return dt
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def has_date_elements(cell):
|
|
146
|
+
"""
|
|
147
|
+
Try to use formatting to determine if a cell contains only time info.
|
|
148
|
+
|
|
149
|
+
See: http://office.microsoft.com/en-us/excel-help/number-format-codes-HP005198679.aspx
|
|
150
|
+
"""
|
|
151
|
+
return 'd' in cell.number_format or 'y' in cell.number_format
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
agate.Table.from_xlsx = classmethod(from_xlsx)
|