numbers-parser 4.17.0.post1__py3-none-any.whl → 4.18.0__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.
- numbers_parser/_cat_numbers.py +15 -4
- numbers_parser/cell.py +3 -15
- numbers_parser/document.py +42 -30
- numbers_parser/exceptions.py +1 -1
- numbers_parser/experimental.py +21 -7
- numbers_parser/formula.py +2 -413
- numbers_parser/model.py +73 -81
- numbers_parser/xrefs.py +0 -2
- {numbers_parser-4.17.0.post1.dist-info → numbers_parser-4.18.0.dist-info}/METADATA +35 -121
- {numbers_parser-4.17.0.post1.dist-info → numbers_parser-4.18.0.dist-info}/RECORD +14 -15
- numbers_parser/tokenizer.py +0 -548
- {numbers_parser-4.17.0.post1.dist-info → numbers_parser-4.18.0.dist-info}/WHEEL +0 -0
- {numbers_parser-4.17.0.post1.dist-info → numbers_parser-4.18.0.dist-info}/entry_points.txt +0 -0
- {numbers_parser-4.17.0.post1.dist-info → numbers_parser-4.18.0.dist-info}/licenses/LICENSE.rst +0 -0
- {numbers_parser-4.17.0.post1.dist-info → numbers_parser-4.18.0.dist-info}/top_level.txt +0 -0
numbers_parser/_cat_numbers.py
CHANGED
|
@@ -16,11 +16,19 @@ from numbers_parser import (
|
|
|
16
16
|
)
|
|
17
17
|
from numbers_parser import __name__ as numbers_parser_name
|
|
18
18
|
from numbers_parser.constants import MAX_SIGNIFICANT_DIGITS
|
|
19
|
-
from numbers_parser.experimental import
|
|
19
|
+
from numbers_parser.experimental import ExperimentalFeatures, enable_experimental_feature
|
|
20
20
|
|
|
21
21
|
logger = logging.getLogger(numbers_parser_name)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def experimental_feature_choice(s: str) -> ExperimentalFeatures:
|
|
25
|
+
try:
|
|
26
|
+
return ExperimentalFeatures[s]
|
|
27
|
+
except KeyError:
|
|
28
|
+
msg = f"invalid experimental feature: {s}"
|
|
29
|
+
raise argparse.ArgumentTypeError(msg) # noqa: B904
|
|
30
|
+
|
|
31
|
+
|
|
24
32
|
def command_line_parser():
|
|
25
33
|
parser = argparse.ArgumentParser(
|
|
26
34
|
description="Export data from Apple Numbers spreadsheet tables",
|
|
@@ -70,10 +78,13 @@ def command_line_parser():
|
|
|
70
78
|
)
|
|
71
79
|
parser.add_argument("document", nargs="*", help="Document(s) to export")
|
|
72
80
|
parser.add_argument("--debug", default=False, action="store_true", help="Enable debug logging")
|
|
81
|
+
experimental_choices = [
|
|
82
|
+
f.name for f in ExperimentalFeatures if f is not ExperimentalFeatures.NONE
|
|
83
|
+
]
|
|
73
84
|
parser.add_argument(
|
|
74
85
|
"--experimental",
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
type=experimental_feature_choice,
|
|
87
|
+
choices=[ExperimentalFeatures[name] for name in experimental_choices],
|
|
77
88
|
help=argparse.SUPPRESS,
|
|
78
89
|
)
|
|
79
90
|
return parser
|
|
@@ -136,7 +147,7 @@ def main() -> None:
|
|
|
136
147
|
else:
|
|
137
148
|
logger.setLevel("ERROR")
|
|
138
149
|
if args.experimental:
|
|
139
|
-
|
|
150
|
+
enable_experimental_feature(args.experimental)
|
|
140
151
|
for filename in args.document:
|
|
141
152
|
try:
|
|
142
153
|
if args.list_sheets:
|
numbers_parser/cell.py
CHANGED
|
@@ -56,7 +56,6 @@ from numbers_parser.constants import (
|
|
|
56
56
|
)
|
|
57
57
|
from numbers_parser.currencies import CURRENCIES, CURRENCY_SYMBOLS
|
|
58
58
|
from numbers_parser.exceptions import UnsupportedError, UnsupportedWarning
|
|
59
|
-
from numbers_parser.formula import Formula
|
|
60
59
|
from numbers_parser.generated import TSPMessages_pb2 as TSPMessages
|
|
61
60
|
from numbers_parser.generated import TSTArchives_pb2 as TSTArchives
|
|
62
61
|
from numbers_parser.generated.TSWPArchives_pb2 import (
|
|
@@ -257,7 +256,7 @@ class Style:
|
|
|
257
256
|
"""
|
|
258
257
|
|
|
259
258
|
alignment: Alignment = DEFAULT_ALIGNMENT_CLASS # : horizontal and vertical alignment
|
|
260
|
-
bg_image: object = None # :
|
|
259
|
+
bg_image: object = None # : background image
|
|
261
260
|
bg_color: RGB | list[RGB] = None
|
|
262
261
|
font_color: RGB = field(default_factory=default_color)
|
|
263
262
|
font_size: float = DEFAULT_FONT_SIZE
|
|
@@ -651,7 +650,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
651
650
|
Returns
|
|
652
651
|
-------
|
|
653
652
|
str:
|
|
654
|
-
The text of the
|
|
653
|
+
The text of the formula in a cell, or `None` if there is no formula
|
|
655
654
|
present in a cell.
|
|
656
655
|
|
|
657
656
|
"""
|
|
@@ -660,17 +659,6 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
660
659
|
return table_formulas.formula(self._formula_id, self.row, self.col)
|
|
661
660
|
return None
|
|
662
661
|
|
|
663
|
-
@formula.setter
|
|
664
|
-
def formula(self, value: str) -> None:
|
|
665
|
-
self._formula_id = Formula.from_str(
|
|
666
|
-
self._model,
|
|
667
|
-
self._table_id,
|
|
668
|
-
self.row,
|
|
669
|
-
self.col,
|
|
670
|
-
value,
|
|
671
|
-
)
|
|
672
|
-
self._model.add_formula_dependency(self.row, self.col, self._table_id)
|
|
673
|
-
|
|
674
662
|
@property
|
|
675
663
|
def is_bulleted(self) -> bool:
|
|
676
664
|
"""bool: ``True`` if the cell contains text bullets."""
|
|
@@ -933,7 +921,7 @@ class Cell(CellStorageFlags, Cacheable):
|
|
|
933
921
|
elif cell_type == CURRENCY_CELL_TYPE:
|
|
934
922
|
cell = NumberCell(row, col, d128, cell_type=CellType.CURRENCY)
|
|
935
923
|
else:
|
|
936
|
-
msg = f"Cell type ID {cell_type} is not
|
|
924
|
+
msg = f"Cell type ID {cell_type} is not recognized"
|
|
937
925
|
raise UnsupportedError(msg)
|
|
938
926
|
|
|
939
927
|
cell._copy_flags(storage_flags)
|
numbers_parser/document.py
CHANGED
|
@@ -45,7 +45,7 @@ class Document:
|
|
|
45
45
|
Create an instance of a new Numbers document.
|
|
46
46
|
|
|
47
47
|
If ``filename`` is ``None``, an empty document is created using the defaults
|
|
48
|
-
defined by the class constructor. You can
|
|
48
|
+
defined by the class constructor. You can optionally override these
|
|
49
49
|
defaults at object construction time.
|
|
50
50
|
|
|
51
51
|
Parameters
|
|
@@ -303,7 +303,7 @@ class Document:
|
|
|
303
303
|
* **num_decimals** (``int``, *optional*, default: ``0``) - Integer precision
|
|
304
304
|
when decimals are padded.
|
|
305
305
|
* **show_thousands_separator** (``bool``, *optional*, default: ``False``) - ``True``
|
|
306
|
-
if the number should include a thousands
|
|
306
|
+
if the number should include a thousands separator.
|
|
307
307
|
|
|
308
308
|
:``"datetime"``:
|
|
309
309
|
* **format** (``str``, *optional*, default: ``"d MMM y"``) - A POSIX strftime-like
|
|
@@ -327,7 +327,7 @@ class Document:
|
|
|
327
327
|
try:
|
|
328
328
|
kwargs["type"] = CustomFormattingType[format_type]
|
|
329
329
|
except (KeyError, AttributeError):
|
|
330
|
-
msg = f"
|
|
330
|
+
msg = f"unsupported cell format type '{format_type}'"
|
|
331
331
|
raise TypeError(msg) from None
|
|
332
332
|
|
|
333
333
|
custom_format = CustomFormatting(**kwargs)
|
|
@@ -474,7 +474,7 @@ class Table(Cacheable):
|
|
|
474
474
|
self._table_id = table_id
|
|
475
475
|
self.num_rows = self._model.number_of_rows(self._table_id)
|
|
476
476
|
self.num_cols = self._model.number_of_columns(self._table_id)
|
|
477
|
-
# Cache all data now to
|
|
477
|
+
# Cache all data now to facilitate write(). Performance impact
|
|
478
478
|
# of computing all cells is minimal compared to file IO
|
|
479
479
|
self._data = []
|
|
480
480
|
self._model.set_table_data(table_id, self._data)
|
|
@@ -545,7 +545,7 @@ class Table(Cacheable):
|
|
|
545
545
|
------
|
|
546
546
|
ValueError:
|
|
547
547
|
If the number of headers is negative, exceeds the number of rows in the
|
|
548
|
-
table, or exceeds Numbers
|
|
548
|
+
table, or exceeds Numbers maximum number of headers (``MAX_HEADER_COUNT``).
|
|
549
549
|
|
|
550
550
|
"""
|
|
551
551
|
return self._model.num_header_rows(self._table_id)
|
|
@@ -579,7 +579,7 @@ class Table(Cacheable):
|
|
|
579
579
|
------
|
|
580
580
|
ValueError:
|
|
581
581
|
If the number of headers is negative, exceeds the number of rows in the
|
|
582
|
-
table, or exceeds Numbers
|
|
582
|
+
table, or exceeds Numbers maximum number of headers (``MAX_HEADER_COUNT``).
|
|
583
583
|
|
|
584
584
|
"""
|
|
585
585
|
return self._model.num_header_cols(self._table_id)
|
|
@@ -756,6 +756,8 @@ class Table(Cacheable):
|
|
|
756
756
|
msg = f"column {col} out of range"
|
|
757
757
|
raise IndexError(msg)
|
|
758
758
|
|
|
759
|
+
if (row_mapper := self._model.table_category_row_map(self._table_id)) is not None:
|
|
760
|
+
return self._data[row_mapper[row]][col]
|
|
759
761
|
return self._data[row][col]
|
|
760
762
|
|
|
761
763
|
def iter_rows(
|
|
@@ -803,10 +805,10 @@ class Table(Cacheable):
|
|
|
803
805
|
sum += row
|
|
804
806
|
|
|
805
807
|
"""
|
|
806
|
-
min_row = min_row
|
|
807
|
-
max_row = max_row
|
|
808
|
-
min_col = min_col
|
|
809
|
-
max_col = max_col
|
|
808
|
+
min_row = min_row if min_row is not None else 0
|
|
809
|
+
max_row = max_row if max_row is not None else self.num_rows - 1
|
|
810
|
+
min_col = min_col if min_col is not None else 0
|
|
811
|
+
max_col = max_col if max_col is not None else self.num_cols - 1
|
|
810
812
|
|
|
811
813
|
if min_row < 0:
|
|
812
814
|
msg = f"row {min_row} out of range"
|
|
@@ -822,11 +824,16 @@ class Table(Cacheable):
|
|
|
822
824
|
raise IndexError(msg)
|
|
823
825
|
|
|
824
826
|
rows = self.rows()
|
|
825
|
-
|
|
827
|
+
if (row_mapper := self._model.table_category_row_map(self._table_id)) is not None:
|
|
828
|
+
rows = [rows[row_mapper[row]] for row in range(min_row, max_row + 1)]
|
|
829
|
+
else:
|
|
830
|
+
rows = rows[min_row : max_row + 1]
|
|
831
|
+
|
|
832
|
+
for row in rows:
|
|
826
833
|
if values_only:
|
|
827
|
-
yield tuple(cell.value for cell in
|
|
834
|
+
yield tuple(cell.value for cell in row[min_col : max_col + 1])
|
|
828
835
|
else:
|
|
829
|
-
yield tuple(
|
|
836
|
+
yield tuple(row[min_col : max_col + 1])
|
|
830
837
|
|
|
831
838
|
def iter_cols(
|
|
832
839
|
self,
|
|
@@ -873,10 +880,10 @@ class Table(Cacheable):
|
|
|
873
880
|
sum += col.value
|
|
874
881
|
|
|
875
882
|
"""
|
|
876
|
-
min_row = min_row
|
|
877
|
-
max_row = max_row
|
|
878
|
-
min_col = min_col
|
|
879
|
-
max_col = max_col
|
|
883
|
+
min_row = min_row if min_row is not None else 0
|
|
884
|
+
max_row = max_row if max_row is not None else self.num_rows - 1
|
|
885
|
+
min_col = min_col if min_col is not None else 0
|
|
886
|
+
max_col = max_col if max_col is not None else self.num_cols - 1
|
|
880
887
|
|
|
881
888
|
if min_row < 0:
|
|
882
889
|
msg = f"row {min_row} out of range"
|
|
@@ -892,11 +899,16 @@ class Table(Cacheable):
|
|
|
892
899
|
raise IndexError(msg)
|
|
893
900
|
|
|
894
901
|
rows = self.rows()
|
|
902
|
+
if (row_mapper := self._model.table_category_row_map(self._table_id)) is not None:
|
|
903
|
+
rows = [rows[row_mapper[row_num]] for row_num in range(min_row, max_row + 1)]
|
|
904
|
+
else:
|
|
905
|
+
rows = rows[min_row : max_row + 1]
|
|
906
|
+
|
|
895
907
|
for col in range(min_col, max_col + 1):
|
|
896
908
|
if values_only:
|
|
897
|
-
yield tuple(row[col].value for row in rows
|
|
909
|
+
yield tuple(row[col].value for row in rows)
|
|
898
910
|
else:
|
|
899
|
-
yield tuple(row[col] for row in rows
|
|
911
|
+
yield tuple(row[col] for row in rows)
|
|
900
912
|
|
|
901
913
|
def _validate_cell_coords(self, *args):
|
|
902
914
|
if isinstance(args[0], str):
|
|
@@ -961,7 +973,7 @@ class Table(Cacheable):
|
|
|
961
973
|
Raises
|
|
962
974
|
------
|
|
963
975
|
IndexError:
|
|
964
|
-
If the style name cannot be
|
|
976
|
+
If the style name cannot be found in the document.
|
|
965
977
|
TypeError:
|
|
966
978
|
If the style parameter is an invalid type.
|
|
967
979
|
ValueError:
|
|
@@ -1001,8 +1013,8 @@ class Table(Cacheable):
|
|
|
1001
1013
|
|
|
1002
1014
|
def categorized_data(self) -> dict | None:
|
|
1003
1015
|
"""
|
|
1004
|
-
Return the table's data
|
|
1005
|
-
if the table has not had
|
|
1016
|
+
Return the table's data organized into categories, if enabled or ``None``
|
|
1017
|
+
if the table has not had categories enabled.
|
|
1006
1018
|
|
|
1007
1019
|
The data is a dictionary with the category names as keys and a list
|
|
1008
1020
|
dictionaries for each row in that category of the table. The table heading
|
|
@@ -1279,7 +1291,7 @@ class Table(Cacheable):
|
|
|
1279
1291
|
"""
|
|
1280
1292
|
Set the borders for a cell.
|
|
1281
1293
|
|
|
1282
|
-
Cell references can be row-column
|
|
1294
|
+
Cell references can be row-column offsets or Excel/Numbers-style A1 notation. Borders
|
|
1283
1295
|
can be applied to multiple sides of a cell by passing a list of sides. The name(s)
|
|
1284
1296
|
of the side(s) must be one of ``"top"``, ``"right"``, ``"bottom"`` or ``"left"``.
|
|
1285
1297
|
|
|
@@ -1299,7 +1311,7 @@ class Table(Cacheable):
|
|
|
1299
1311
|
* **param2** (*int*): The column number (zero indexed).
|
|
1300
1312
|
* **param3** (*str | List[str]*): Which side(s) of the cell to apply the border to.
|
|
1301
1313
|
* **param4** (:py:class:`Border`): The border to add.
|
|
1302
|
-
* **param5** (*int*, *
|
|
1314
|
+
* **param5** (*int*, *optional*, default: 1): The length of the stroke to add.
|
|
1303
1315
|
|
|
1304
1316
|
:Args (A1):
|
|
1305
1317
|
* **param1** (*str*): A cell reference using Excel/Numbers-style A1 notation.
|
|
@@ -1380,7 +1392,7 @@ class Table(Cacheable):
|
|
|
1380
1392
|
r"""
|
|
1381
1393
|
Set the data format for a cell.
|
|
1382
1394
|
|
|
1383
|
-
Cell references can be **row-column**
|
|
1395
|
+
Cell references can be **row-column** offsets or Excel/Numbers-style **A1** notation.
|
|
1384
1396
|
|
|
1385
1397
|
.. code:: python
|
|
1386
1398
|
|
|
@@ -1463,8 +1475,8 @@ class Table(Cacheable):
|
|
|
1463
1475
|
decimal places, or ``None`` for automatic.
|
|
1464
1476
|
|
|
1465
1477
|
:``"custom"``:
|
|
1466
|
-
* **format** (*str |
|
|
1467
|
-
|
|
1478
|
+
* **format** (*str | CustomFormatting*) - The name of a custom
|
|
1479
|
+
formatting the document or a :py:class:`~numbers_parser.CustomFormatting`
|
|
1468
1480
|
object.
|
|
1469
1481
|
|
|
1470
1482
|
:``"currency"``:
|
|
@@ -1475,7 +1487,7 @@ class Table(Cacheable):
|
|
|
1475
1487
|
* **negative_style** (*:py:class:`~numbers_parser.NegativeNumberStyle`, optional, default: NegativeNumberStyle.MINUS*) - How negative numbers are represented.
|
|
1476
1488
|
See `Negative number formats <#negative-formats>`_.
|
|
1477
1489
|
* **show_thousands_separator** (*bool, optional, default: False*) - ``True``
|
|
1478
|
-
if the number should include a thousands
|
|
1490
|
+
if the number should include a thousands separator, e.g. ``,``
|
|
1479
1491
|
* **use_accounting_style** (*bool, optional, default: False*) - ``True``
|
|
1480
1492
|
if the currency symbol should be formatted to the left of the cell and
|
|
1481
1493
|
separated from the number value by a tab.
|
|
@@ -1495,7 +1507,7 @@ class Table(Cacheable):
|
|
|
1495
1507
|
* **negative_style** (*:py:class:`~numbers_parser.NegativeNumberStyle`, optional, default: NegativeNumberStyle.MINUS*) - How negative numbers are represented.
|
|
1496
1508
|
See `Negative number formats <#negative-formats>`_.
|
|
1497
1509
|
* **show_thousands_separator** (*bool, optional, default: False*) - ``True``
|
|
1498
|
-
if the number should include a thousands
|
|
1510
|
+
if the number should include a thousands separator, e.g. ``,``
|
|
1499
1511
|
|
|
1500
1512
|
:``"scientific"``:
|
|
1501
1513
|
* **decimal_places** (*float, optional, default: None*) - number of
|
|
@@ -1586,7 +1598,7 @@ class Table(Cacheable):
|
|
|
1586
1598
|
format_type = FormattingType[format_type_name.upper()]
|
|
1587
1599
|
_ = FORMATTING_ALLOWED_CELLS[format_type_name]
|
|
1588
1600
|
except (KeyError, AttributeError):
|
|
1589
|
-
msg = f"
|
|
1601
|
+
msg = f"unsupported cell format type '{format_type_name}'"
|
|
1590
1602
|
raise TypeError(msg) from None
|
|
1591
1603
|
|
|
1592
1604
|
cell = self._data[row][col]
|
numbers_parser/exceptions.py
CHANGED
numbers_parser/experimental.py
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from enum import Flag, auto
|
|
2
3
|
|
|
3
4
|
logger = logging.getLogger(__name__)
|
|
4
5
|
debug = logger.debug
|
|
5
6
|
|
|
6
|
-
_EXPERIMENTAL_FEATURES = False
|
|
7
7
|
|
|
8
|
+
class ExperimentalFeatures(Flag):
|
|
9
|
+
NONE = auto()
|
|
10
|
+
TESTING = auto()
|
|
11
|
+
GROUPED_CATEGORIES = auto()
|
|
8
12
|
|
|
9
|
-
def _enable_experimental_features(status: bool) -> None:
|
|
10
|
-
global _EXPERIMENTAL_FEATURES
|
|
11
|
-
_EXPERIMENTAL_FEATURES = status
|
|
12
|
-
debug("Experimental features %s", "on" if status else "off")
|
|
13
13
|
|
|
14
|
+
EXPERIMENTAL_FEATURES = ExperimentalFeatures.NONE
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
def enable_experimental_feature(flags: ExperimentalFeatures) -> None:
|
|
18
|
+
global EXPERIMENTAL_FEATURES
|
|
19
|
+
EXPERIMENTAL_FEATURES |= flags
|
|
20
|
+
debug("Experimental features: enabling %s, flags=%s", flags, EXPERIMENTAL_FEATURES)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def disable_experimental_feature(flags: ExperimentalFeatures) -> None:
|
|
24
|
+
global EXPERIMENTAL_FEATURES
|
|
25
|
+
EXPERIMENTAL_FEATURES ^= flags
|
|
26
|
+
debug("Experimental features: disabling %s, flags=%s", flags, EXPERIMENTAL_FEATURES)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def experimental_features() -> ExperimentalFeatures:
|
|
30
|
+
return EXPERIMENTAL_FEATURES
|