numbers-parser 4.17.0.post1__py3-none-any.whl → 4.18.1__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.
@@ -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 _enable_experimental_features
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
- default=False,
76
- action="store_true",
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
- _enable_experimental_features(True)
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 # : backgroung image
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 foruma in a cell, or `None` if there is no formula
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 recognised"
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)
@@ -121,6 +121,7 @@ def _week_of_month(value: datetime) -> int:
121
121
 
122
122
  DATETIME_FIELD_MAP = OrderedDict(
123
123
  [
124
+ # Cell formats
124
125
  ("a", lambda x: x.strftime("%p").lower()),
125
126
  ("EEEE", "%A"),
126
127
  ("EEE", "%a"),
@@ -157,6 +158,10 @@ DATETIME_FIELD_MAP = OrderedDict(
157
158
  ("SSS", lambda x: str(x.microsecond).zfill(6)[0:3]),
158
159
  ("SSSS", lambda x: str(x.microsecond).zfill(6)[0:4]),
159
160
  ("SSSSS", lambda x: str(x.microsecond).zfill(6)[0:5]),
161
+ # Table category formats
162
+ ("QQQ", lambda x: "Q" + str(int(x.month / 3) + 1)),
163
+ ("LLLL", "%B"),
164
+ ("w", "%-W"),
160
165
  ],
161
166
  )
162
167
 
@@ -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 optionionally override these
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 seperator.
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"unsuported cell format type '{format_type}'"
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 facilite write(). Performance impact
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 maxinum number of headers (``MAX_HEADER_COUNT``).
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 maxinum number of headers (``MAX_HEADER_COUNT``).
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,10 @@ class Table(Cacheable):
756
756
  msg = f"column {col} out of range"
757
757
  raise IndexError(msg)
758
758
 
759
+ self._model.calculate_table_categories(self._table_id)
760
+ row_mapper = self._model._table_categories_row_mapper[self._table_id]
761
+ if row_mapper is not None:
762
+ return self._data[row_mapper[row]][col]
759
763
  return self._data[row][col]
760
764
 
761
765
  def iter_rows(
@@ -803,10 +807,10 @@ class Table(Cacheable):
803
807
  sum += row
804
808
 
805
809
  """
806
- min_row = min_row or 0
807
- max_row = max_row or self.num_rows - 1
808
- min_col = min_col or 0
809
- max_col = max_col or self.num_cols - 1
810
+ min_row = min_row if min_row is not None else 0
811
+ max_row = max_row if max_row is not None else self.num_rows - 1
812
+ min_col = min_col if min_col is not None else 0
813
+ max_col = max_col if max_col is not None else self.num_cols - 1
810
814
 
811
815
  if min_row < 0:
812
816
  msg = f"row {min_row} out of range"
@@ -822,11 +826,18 @@ class Table(Cacheable):
822
826
  raise IndexError(msg)
823
827
 
824
828
  rows = self.rows()
825
- for row in range(min_row, max_row + 1):
829
+ self._model.calculate_table_categories(self._table_id)
830
+ row_mapper = self._model._table_categories_row_mapper[self._table_id]
831
+ if row_mapper is not None:
832
+ rows = [rows[row_mapper[row]] for row in range(min_row, max_row + 1)]
833
+ else:
834
+ rows = rows[min_row : max_row + 1]
835
+
836
+ for row in rows:
826
837
  if values_only:
827
- yield tuple(cell.value for cell in rows[row][min_col : max_col + 1])
838
+ yield tuple(cell.value for cell in row[min_col : max_col + 1])
828
839
  else:
829
- yield tuple(rows[row][min_col : max_col + 1])
840
+ yield tuple(row[min_col : max_col + 1])
830
841
 
831
842
  def iter_cols(
832
843
  self,
@@ -873,10 +884,10 @@ class Table(Cacheable):
873
884
  sum += col.value
874
885
 
875
886
  """
876
- min_row = min_row or 0
877
- max_row = max_row or self.num_rows - 1
878
- min_col = min_col or 0
879
- max_col = max_col or self.num_cols - 1
887
+ min_row = min_row if min_row is not None else 0
888
+ max_row = max_row if max_row is not None else self.num_rows - 1
889
+ min_col = min_col if min_col is not None else 0
890
+ max_col = max_col if max_col is not None else self.num_cols - 1
880
891
 
881
892
  if min_row < 0:
882
893
  msg = f"row {min_row} out of range"
@@ -892,11 +903,18 @@ class Table(Cacheable):
892
903
  raise IndexError(msg)
893
904
 
894
905
  rows = self.rows()
906
+ self._model.calculate_table_categories(self._table_id)
907
+ row_mapper = self._model._table_categories_row_mapper[self._table_id]
908
+ if row_mapper is not None:
909
+ rows = [rows[row_mapper[row_num]] for row_num in range(min_row, max_row + 1)]
910
+ else:
911
+ rows = rows[min_row : max_row + 1]
912
+
895
913
  for col in range(min_col, max_col + 1):
896
914
  if values_only:
897
- yield tuple(row[col].value for row in rows[min_row : max_row + 1])
915
+ yield tuple(row[col].value for row in rows)
898
916
  else:
899
- yield tuple(row[col] for row in rows[min_row : max_row + 1])
917
+ yield tuple(row[col] for row in rows)
900
918
 
901
919
  def _validate_cell_coords(self, *args):
902
920
  if isinstance(args[0], str):
@@ -961,7 +979,7 @@ class Table(Cacheable):
961
979
  Raises
962
980
  ------
963
981
  IndexError:
964
- If the style name cannot be foiund in the document.
982
+ If the style name cannot be found in the document.
965
983
  TypeError:
966
984
  If the style parameter is an invalid type.
967
985
  ValueError:
@@ -999,33 +1017,60 @@ class Table(Cacheable):
999
1017
  msg = "style must be a Style object or style name"
1000
1018
  raise TypeError(msg)
1001
1019
 
1002
- def categorized_data(self) -> dict | None:
1020
+ def categorized_data(self, values_only: bool = False) -> dict | None:
1003
1021
  """
1004
- Return the table's data organised into categories, if enabled or ``None``
1005
- if the table has not had categoried enabled.
1022
+ Return the table's data organized into categories, if enabled or ``None``
1023
+ if the table has not had categories enabled.
1024
+
1025
+ The data is a set of nested dictionaries and lists. Dictionary keys are
1026
+ Category keys and values are either another dictionary in the case of
1027
+ nested categories or a list of rows. Each row is itself a list such that
1028
+ the data is the same as returned by :py:meth:`numbers_parser.Table.rows`.
1006
1029
 
1007
- The data is a dictionary with the category names as keys and a list
1008
- dictionaries for each row in that category of the table. The table heading
1009
- row is used as the keys for the row dictionary.
1030
+ Parameters
1031
+ ----------
1032
+ values_only:
1033
+ If ``True``, return cell values instead of :class:`Cell` objects
1034
+
1035
+ Returns
1036
+ -------
1037
+ Dict[str, Dict | List]:
1038
+ Nested dictionary of lists of rows or dictionaries of the next group
1039
+ of categories down. Row data is returned as :py:class:`Cell` classes
1040
+ unless ``values_only`` is ``True``.
1010
1041
 
1011
1042
  Example
1012
1043
  -------
1013
1044
  .. code:: python
1014
1045
 
1015
1046
  "Transport": [
1016
- {"Description": "Airplane", "Category": "Transport" },
1017
- {"Description": "Bicycle", "Category": "Transport" },
1018
- {"Description": "Bus", "Category": "Transport"},
1047
+ {"Airplane", "Air": 5 },
1048
+ {"Helicopter": "Air", 2 },
1049
+ {"Bus": "Road", 10 },
1019
1050
  ],
1020
1051
  "Fruit": [
1021
- {"Description": "Apple", "Category": "Fruit" },
1022
- {"Description": "Banana", "Category": "Fruit" },
1052
+ {"Apple", "Green": 7 },
1053
+ {"Banana", "Yellow", 6 },
1023
1054
  ],
1024
1055
 
1025
1056
  For tables with multiple categories, the top-level dictionary is nested.
1026
1057
 
1027
1058
  """
1028
- return self._model.table_category_data(self._table_id)
1059
+
1060
+ def data_to_values(item):
1061
+ if isinstance(item, dict):
1062
+ return {k: data_to_values(v) for k, v in item.items()}
1063
+
1064
+ if isinstance(item, list):
1065
+ return [data_to_values(v) for v in item]
1066
+
1067
+ return item.value
1068
+
1069
+ self._model.calculate_table_categories(self._table_id)
1070
+ data = self._model._table_categories_data[self._table_id]
1071
+ if values_only:
1072
+ return data_to_values(data)
1073
+ return data
1029
1074
 
1030
1075
  def add_row(
1031
1076
  self,
@@ -1279,7 +1324,7 @@ class Table(Cacheable):
1279
1324
  """
1280
1325
  Set the borders for a cell.
1281
1326
 
1282
- Cell references can be row-column offsers or Excel/Numbers-style A1 notation. Borders
1327
+ Cell references can be row-column offsets or Excel/Numbers-style A1 notation. Borders
1283
1328
  can be applied to multiple sides of a cell by passing a list of sides. The name(s)
1284
1329
  of the side(s) must be one of ``"top"``, ``"right"``, ``"bottom"`` or ``"left"``.
1285
1330
 
@@ -1299,7 +1344,7 @@ class Table(Cacheable):
1299
1344
  * **param2** (*int*): The column number (zero indexed).
1300
1345
  * **param3** (*str | List[str]*): Which side(s) of the cell to apply the border to.
1301
1346
  * **param4** (:py:class:`Border`): The border to add.
1302
- * **param5** (*int*, *optinal*, default: 1): The length of the stroke to add.
1347
+ * **param5** (*int*, *optional*, default: 1): The length of the stroke to add.
1303
1348
 
1304
1349
  :Args (A1):
1305
1350
  * **param1** (*str*): A cell reference using Excel/Numbers-style A1 notation.
@@ -1380,7 +1425,7 @@ class Table(Cacheable):
1380
1425
  r"""
1381
1426
  Set the data format for a cell.
1382
1427
 
1383
- Cell references can be **row-column** offsers or Excel/Numbers-style **A1** notation.
1428
+ Cell references can be **row-column** offsets or Excel/Numbers-style **A1** notation.
1384
1429
 
1385
1430
  .. code:: python
1386
1431
 
@@ -1463,8 +1508,8 @@ class Table(Cacheable):
1463
1508
  decimal places, or ``None`` for automatic.
1464
1509
 
1465
1510
  :``"custom"``:
1466
- * **format** (*str | CustomFormating*) - The name of a custom
1467
- formatin the document or a :py:class:`~numbers_parser.CustomFormatting`
1511
+ * **format** (*str | CustomFormatting*) - The name of a custom
1512
+ formatting the document or a :py:class:`~numbers_parser.CustomFormatting`
1468
1513
  object.
1469
1514
 
1470
1515
  :``"currency"``:
@@ -1475,7 +1520,7 @@ class Table(Cacheable):
1475
1520
  * **negative_style** (*:py:class:`~numbers_parser.NegativeNumberStyle`, optional, default: NegativeNumberStyle.MINUS*) - How negative numbers are represented.
1476
1521
  See `Negative number formats <#negative-formats>`_.
1477
1522
  * **show_thousands_separator** (*bool, optional, default: False*) - ``True``
1478
- if the number should include a thousands seperator, e.g. ``,``
1523
+ if the number should include a thousands separator, e.g. ``,``
1479
1524
  * **use_accounting_style** (*bool, optional, default: False*) - ``True``
1480
1525
  if the currency symbol should be formatted to the left of the cell and
1481
1526
  separated from the number value by a tab.
@@ -1495,7 +1540,7 @@ class Table(Cacheable):
1495
1540
  * **negative_style** (*:py:class:`~numbers_parser.NegativeNumberStyle`, optional, default: NegativeNumberStyle.MINUS*) - How negative numbers are represented.
1496
1541
  See `Negative number formats <#negative-formats>`_.
1497
1542
  * **show_thousands_separator** (*bool, optional, default: False*) - ``True``
1498
- if the number should include a thousands seperator, e.g. ``,``
1543
+ if the number should include a thousands separator, e.g. ``,``
1499
1544
 
1500
1545
  :``"scientific"``:
1501
1546
  * **decimal_places** (*float, optional, default: None*) - number of
@@ -1586,7 +1631,7 @@ class Table(Cacheable):
1586
1631
  format_type = FormattingType[format_type_name.upper()]
1587
1632
  _ = FORMATTING_ALLOWED_CELLS[format_type_name]
1588
1633
  except (KeyError, AttributeError):
1589
- msg = f"unsuported cell format type '{format_type_name}'"
1634
+ msg = f"unsupported cell format type '{format_type_name}'"
1590
1635
  raise TypeError(msg) from None
1591
1636
 
1592
1637
  cell = self._data[row][col]
@@ -7,7 +7,7 @@ class UnsupportedError(NumbersError):
7
7
 
8
8
 
9
9
  class NotImplementedError(NumbersError):
10
- """Raised for unsuported Protobufs/Formats."""
10
+ """Raised for unsupported Protobufs/Formats."""
11
11
 
12
12
 
13
13
  class FileError(NumbersError):
@@ -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
- def _experimental_features() -> bool:
16
- return _EXPERIMENTAL_FEATURES
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