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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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
@@ -0,0 +1,2 @@
1
+ import agateexcel.table_xls
2
+ import agateexcel.table_xlsx
@@ -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)
@@ -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)