numbers-parser 4.18.0__tar.gz → 4.18.1__tar.gz
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-4.18.0/src/numbers_parser.egg-info → numbers_parser-4.18.1}/PKG-INFO +1 -1
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/pyproject.toml +2 -2
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/constants.py +5 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/document.py +46 -13
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/model.py +112 -94
- {numbers_parser-4.18.0 → numbers_parser-4.18.1/src/numbers_parser.egg-info}/PKG-INFO +1 -1
- numbers_parser-4.18.1/tests/test_categories.py +391 -0
- numbers_parser-4.18.0/tests/test_categories.py +0 -712
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/LICENSE.rst +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/README.md +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/setup.cfg +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/__init__.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/_cat_numbers.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/_csv2numbers.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/_unpack_numbers.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/bullets.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/cell.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/containers.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/currencies.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/data/empty.numbers +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/exceptions.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/experimental.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/formula.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TNArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TNArchives_sos_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TNCommandArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TNCommandArchives_sos_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSAArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSAArchives_sos_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSACommandArchives_sos_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSCEArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSCH3DArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSCHArchives_Common_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSCHArchives_GEN_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSCHArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSCHArchives_sos_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSCHCommandArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSCHPreUFFArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSCKArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSCKArchives_sos_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSDArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSDArchives_sos_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSDCommandArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSKArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSPArchiveMessages_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSPDatabaseMessages_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSPMessages_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSSArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSSArchives_sos_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSTArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSTArchives_sos_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSTCommandArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSTStylePropertyArchiving_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSWPArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSWPArchives_sos_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/TSWPCommandArchives_pb2.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/__init__.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/fontmap.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/functionmap.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/generated/mapping.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/iwafile.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/iwork.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/numbers_cache.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/numbers_uuid.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/roman.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser/xrefs.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser.egg-info/SOURCES.txt +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser.egg-info/dependency_links.txt +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser.egg-info/entry_points.txt +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser.egg-info/requires.txt +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/src/numbers_parser.egg-info/top_level.txt +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_all_formulas.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_api_change.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_borders.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_bullets.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_cat_numbers.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_coverage.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_create_cells.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_csv2numbers.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_currency.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_folder.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_formatting.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_formulas.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_issues.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_large.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_memory_leaks.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_merges.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_package.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_properties.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_roman.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_save.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_slices.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_styles.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_table_size.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_tables.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_unpack_numbers.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_unsupported.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_uuids.py +0 -0
- {numbers_parser-4.18.0 → numbers_parser-4.18.1}/tests/test_version.py +0 -0
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
description = "Read and write Apple Numbers spreadsheets"
|
|
23
23
|
name = "numbers-parser"
|
|
24
24
|
readme = "README.md"
|
|
25
|
-
version = "4.18.
|
|
25
|
+
version = "4.18.1"
|
|
26
26
|
|
|
27
27
|
[project.urls]
|
|
28
28
|
repository = "https://github.com/masaccio/numbers-parser"
|
|
@@ -197,5 +197,5 @@ ban-relative-imports = "all"
|
|
|
197
197
|
"src/build/**" = ["PLR2004", "INP001", "PTH"]
|
|
198
198
|
"src/build/protodump.py" = ["PLR2004", "INP001", "PTH", "S110", "N806"]
|
|
199
199
|
"src/debug/**" = ["INP001"]
|
|
200
|
-
"tests/**" = ["PLR2004", "S101", "D103", "ANN201", "ANN001"]
|
|
200
|
+
"tests/**" = ["PLR2004", "S101", "D103", "ANN201", "ANN001", "DTZ001"]
|
|
201
201
|
|
|
@@ -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
|
|
|
@@ -756,7 +756,9 @@ class Table(Cacheable):
|
|
|
756
756
|
msg = f"column {col} out of range"
|
|
757
757
|
raise IndexError(msg)
|
|
758
758
|
|
|
759
|
-
|
|
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:
|
|
760
762
|
return self._data[row_mapper[row]][col]
|
|
761
763
|
return self._data[row][col]
|
|
762
764
|
|
|
@@ -824,7 +826,9 @@ class Table(Cacheable):
|
|
|
824
826
|
raise IndexError(msg)
|
|
825
827
|
|
|
826
828
|
rows = self.rows()
|
|
827
|
-
|
|
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:
|
|
828
832
|
rows = [rows[row_mapper[row]] for row in range(min_row, max_row + 1)]
|
|
829
833
|
else:
|
|
830
834
|
rows = rows[min_row : max_row + 1]
|
|
@@ -899,7 +903,9 @@ class Table(Cacheable):
|
|
|
899
903
|
raise IndexError(msg)
|
|
900
904
|
|
|
901
905
|
rows = self.rows()
|
|
902
|
-
|
|
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:
|
|
903
909
|
rows = [rows[row_mapper[row_num]] for row_num in range(min_row, max_row + 1)]
|
|
904
910
|
else:
|
|
905
911
|
rows = rows[min_row : max_row + 1]
|
|
@@ -1011,33 +1017,60 @@ class Table(Cacheable):
|
|
|
1011
1017
|
msg = "style must be a Style object or style name"
|
|
1012
1018
|
raise TypeError(msg)
|
|
1013
1019
|
|
|
1014
|
-
def categorized_data(self) -> dict | None:
|
|
1020
|
+
def categorized_data(self, values_only: bool = False) -> dict | None:
|
|
1015
1021
|
"""
|
|
1016
1022
|
Return the table's data organized into categories, if enabled or ``None``
|
|
1017
1023
|
if the table has not had categories enabled.
|
|
1018
1024
|
|
|
1019
|
-
The data is a
|
|
1020
|
-
|
|
1021
|
-
|
|
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`.
|
|
1029
|
+
|
|
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``.
|
|
1022
1041
|
|
|
1023
1042
|
Example
|
|
1024
1043
|
-------
|
|
1025
1044
|
.. code:: python
|
|
1026
1045
|
|
|
1027
1046
|
"Transport": [
|
|
1028
|
-
{"
|
|
1029
|
-
{"
|
|
1030
|
-
{"
|
|
1047
|
+
{"Airplane", "Air": 5 },
|
|
1048
|
+
{"Helicopter": "Air", 2 },
|
|
1049
|
+
{"Bus": "Road", 10 },
|
|
1031
1050
|
],
|
|
1032
1051
|
"Fruit": [
|
|
1033
|
-
{"
|
|
1034
|
-
{"
|
|
1052
|
+
{"Apple", "Green": 7 },
|
|
1053
|
+
{"Banana", "Yellow", 6 },
|
|
1035
1054
|
],
|
|
1036
1055
|
|
|
1037
1056
|
For tables with multiple categories, the top-level dictionary is nested.
|
|
1038
1057
|
|
|
1039
1058
|
"""
|
|
1040
|
-
|
|
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
|
|
1041
1074
|
|
|
1042
1075
|
def add_row(
|
|
1043
1076
|
self,
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import re
|
|
4
4
|
from array import array
|
|
5
5
|
from collections import defaultdict
|
|
6
|
+
from datetime import timedelta
|
|
6
7
|
from hashlib import sha1
|
|
7
8
|
from itertools import chain
|
|
8
9
|
from math import floor
|
|
@@ -32,6 +33,7 @@ from numbers_parser.cell import (
|
|
|
32
33
|
PaddingType,
|
|
33
34
|
Style,
|
|
34
35
|
VerticalJustification,
|
|
36
|
+
_decode_date_format,
|
|
35
37
|
)
|
|
36
38
|
from numbers_parser.constants import (
|
|
37
39
|
ALLOWED_FORMATTING_PARAMETERS,
|
|
@@ -47,6 +49,7 @@ from numbers_parser.constants import (
|
|
|
47
49
|
DEFAULT_TEXT_WRAP,
|
|
48
50
|
DEFAULT_TILE_SIZE,
|
|
49
51
|
DOCUMENT_ID,
|
|
52
|
+
EPOCH,
|
|
50
53
|
FORMAT_TYPE_MAP,
|
|
51
54
|
MAX_TILE_SIZE,
|
|
52
55
|
PACKAGE_ID,
|
|
@@ -233,6 +236,8 @@ class _NumbersModel(Cacheable):
|
|
|
233
236
|
self._control_specs = DataLists(self, "control_cell_spec_table", "cell_spec")
|
|
234
237
|
self._formulas = DataLists(self, "formula_table", "formula")
|
|
235
238
|
self._table_data = {}
|
|
239
|
+
self._table_categories_data = {}
|
|
240
|
+
self._table_categories_row_mapper = {}
|
|
236
241
|
self._styles = None
|
|
237
242
|
self._images = {}
|
|
238
243
|
self._custom_formats = None
|
|
@@ -2561,8 +2566,18 @@ class _NumbersModel(Cacheable):
|
|
|
2561
2566
|
return cell_value.number_value.value
|
|
2562
2567
|
if cell_value_type == CellValueType.BOOLEAN_TYPE:
|
|
2563
2568
|
return cell_value.boolean_value.value
|
|
2564
|
-
|
|
2565
|
-
|
|
2569
|
+
if cell_value_type == CellValueType.DATE_TYPE:
|
|
2570
|
+
# "yyyy"
|
|
2571
|
+
# "yyyy-QQQ"
|
|
2572
|
+
# "LLLL yyyy"
|
|
2573
|
+
# "yyyy'-W'w"
|
|
2574
|
+
# "d/M/yyyy"
|
|
2575
|
+
# "EEEE"
|
|
2576
|
+
return _decode_date_format(
|
|
2577
|
+
cell_value.date_value.format.date_time_format,
|
|
2578
|
+
EPOCH + timedelta(seconds=cell_value.date_value.value),
|
|
2579
|
+
)
|
|
2580
|
+
return None
|
|
2566
2581
|
|
|
2567
2582
|
@cache(num_args=0)
|
|
2568
2583
|
def group_uuid_values(self):
|
|
@@ -2574,14 +2589,19 @@ class _NumbersModel(Cacheable):
|
|
|
2574
2589
|
}
|
|
2575
2590
|
|
|
2576
2591
|
@cache()
|
|
2577
|
-
def
|
|
2592
|
+
def calculate_table_categories(self, table_id: int) -> tuple[dict[int, int], dict] | None:
|
|
2578
2593
|
category_owner_id = self.objects[table_id].category_owner.identifier
|
|
2579
2594
|
if not category_owner_id:
|
|
2580
|
-
|
|
2595
|
+
self._table_categories_data[table_id] = None
|
|
2596
|
+
self._table_categories_row_mapper[table_id] = None
|
|
2597
|
+
return
|
|
2598
|
+
|
|
2581
2599
|
category_archive_id = self.objects[category_owner_id].group_by[0].identifier
|
|
2582
2600
|
category_archive = self.objects[category_archive_id]
|
|
2583
2601
|
if not category_archive.is_enabled:
|
|
2584
|
-
|
|
2602
|
+
self._table_categories_data[table_id] = None
|
|
2603
|
+
self._table_categories_row_mapper[table_id] = None
|
|
2604
|
+
return
|
|
2585
2605
|
|
|
2586
2606
|
table_info = self.objects[self.table_info_id(table_id)]
|
|
2587
2607
|
category_order = self.objects[table_info.category_order.identifier]
|
|
@@ -2594,101 +2614,99 @@ class _NumbersModel(Cacheable):
|
|
|
2594
2614
|
row_uid_for_index = [
|
|
2595
2615
|
NumbersUUID(row_uid_map.sorted_row_uids[i]) for i in row_uid_map.row_uid_for_index
|
|
2596
2616
|
]
|
|
2597
|
-
return {
|
|
2598
|
-
row: row_uuid_to_offset[uuid]
|
|
2599
|
-
for row, uuid in enumerate(
|
|
2600
|
-
uuid for uuid in row_uid_for_index if uuid not in group_uuids
|
|
2601
|
-
)
|
|
2602
|
-
}
|
|
2603
|
-
|
|
2604
|
-
def table_category_data(self, table_id: int) -> dict | None:
|
|
2605
|
-
category_owner_id = self.objects[table_id].category_owner.identifier
|
|
2606
|
-
category_archive_id = self.objects[category_owner_id].group_by[0].identifier
|
|
2607
|
-
category_archive = self.objects[category_archive_id]
|
|
2608
|
-
if not category_archive.is_enabled:
|
|
2609
|
-
return None
|
|
2610
|
-
|
|
2611
|
-
table_info = self.objects[self.table_info_id(table_id)]
|
|
2612
|
-
category_order = self.objects[table_info.category_order.identifier]
|
|
2613
|
-
row_uid_map = self.objects[category_order.uid_map.identifier]
|
|
2614
|
-
|
|
2615
|
-
sorted_row_uuids = [
|
|
2616
|
-
NumbersUUID(row_uid_map.sorted_row_uids[i]).hex for i in row_uid_map.row_index_for_uid
|
|
2617
|
-
]
|
|
2618
2617
|
|
|
2619
|
-
|
|
2620
|
-
header = [cell.value for cell in data[0]]
|
|
2621
|
-
|
|
2622
|
-
def index_set_to_offsets(index_set: TSCEArchives.IndexSetArchive) -> list[int]:
|
|
2623
|
-
"""Convert an IndexSetArchive to a list of offsets."""
|
|
2624
|
-
offsets = []
|
|
2625
|
-
for entry in index_set.entries:
|
|
2626
|
-
if entry.HasField("range_end"):
|
|
2627
|
-
offsets += list(range(entry.range_begin, entry.range_end + 1))
|
|
2628
|
-
else:
|
|
2629
|
-
offsets += list(range(entry.range_begin, entry.range_begin + 1))
|
|
2630
|
-
return offsets
|
|
2631
|
-
|
|
2632
|
-
group_node_to_key = {
|
|
2633
|
-
NumbersUUID(self.objects[_id].group_uid).hex: _NumbersModel.cell_value_to_key(
|
|
2634
|
-
self.objects[_id].group_cell_value,
|
|
2635
|
-
)
|
|
2636
|
-
for _id in self.find_refs("GroupNodeArchive")
|
|
2637
|
-
}
|
|
2638
|
-
group_uuids = [NumbersUUID(x.group_uid).hex for x in category_archive.group_node_root.child]
|
|
2639
|
-
group_uuids = [uuid for uuid in sorted_row_uuids if uuid in group_uuids]
|
|
2640
|
-
|
|
2641
|
-
def group_hierarchy(parent: str, children: list):
|
|
2642
|
-
nodes = {}
|
|
2618
|
+
def parent_relationships(parent: NumbersUUID, children: list, group_parents: dict):
|
|
2643
2619
|
for child in children:
|
|
2644
|
-
|
|
2620
|
+
child_uuid = NumbersUUID(child.group_uid)
|
|
2621
|
+
group_parents[child_uuid] = parent
|
|
2645
2622
|
if len(child.child) > 0:
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2623
|
+
parent_relationships(child_uuid, child.child, group_parents)
|
|
2624
|
+
|
|
2625
|
+
group_parents = {}
|
|
2626
|
+
parent_relationships(None, category_archive.group_node_root.child, group_parents)
|
|
2627
|
+
|
|
2628
|
+
row = 0
|
|
2629
|
+
row_mapper = {}
|
|
2630
|
+
header = []
|
|
2631
|
+
in_header = True
|
|
2632
|
+
|
|
2633
|
+
nodes: dict[NumbersUUID, dict] = {}
|
|
2634
|
+
root_children: dict = {}
|
|
2635
|
+
stack: list[NumbersUUID | None] = []
|
|
2636
|
+
# rows that are not in any group (rare) kept here
|
|
2637
|
+
root_rows: list = []
|
|
2638
|
+
|
|
2639
|
+
for uuid in row_uid_for_index:
|
|
2640
|
+
if uuid in group_uuids:
|
|
2641
|
+
# this UUID is a group heading
|
|
2642
|
+
in_header = False
|
|
2643
|
+
parent = group_parents.get(uuid)
|
|
2644
|
+
|
|
2645
|
+
# ensure node exists
|
|
2646
|
+
if uuid not in nodes:
|
|
2647
|
+
nodes[uuid] = {
|
|
2648
|
+
"key": group_uuids[uuid],
|
|
2649
|
+
"children": {},
|
|
2650
|
+
"rows": [],
|
|
2671
2651
|
}
|
|
2672
|
-
assign_rows_to_categories(group_uuid, child.child, categories)
|
|
2673
|
-
|
|
2674
|
-
category_tree = group_hierarchy(
|
|
2675
|
-
NumbersUUID(category_archive.group_node_root.group_uid).hex,
|
|
2676
|
-
category_archive.group_node_root.child,
|
|
2677
|
-
)
|
|
2678
2652
|
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
new_tree = {}
|
|
2684
|
-
for k, v in a.items():
|
|
2685
|
-
if v is not None:
|
|
2686
|
-
new_tree[b[k]["key"]] = merge_trees(v, b)
|
|
2653
|
+
# attach node to its parent (or root)
|
|
2654
|
+
if parent is None:
|
|
2655
|
+
if nodes[uuid]["key"] not in root_children:
|
|
2656
|
+
root_children[nodes[uuid]["key"]] = nodes[uuid]
|
|
2687
2657
|
else:
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2658
|
+
if parent not in nodes:
|
|
2659
|
+
nodes[parent] = {
|
|
2660
|
+
"key": group_uuids[parent],
|
|
2661
|
+
"children": {},
|
|
2662
|
+
"rows": [],
|
|
2663
|
+
}
|
|
2664
|
+
parent_node = nodes[parent]
|
|
2665
|
+
if nodes[uuid]["key"] not in parent_node["children"]:
|
|
2666
|
+
parent_node["children"][nodes[uuid]["key"]] = nodes[uuid]
|
|
2667
|
+
|
|
2668
|
+
# update stack to current nesting (pop until parent is on top)
|
|
2669
|
+
while stack and stack[-1] != parent:
|
|
2670
|
+
stack.pop()
|
|
2671
|
+
stack.append(uuid)
|
|
2672
|
+
else:
|
|
2673
|
+
mapped_row = row_uuid_to_offset[uuid]
|
|
2674
|
+
if in_header:
|
|
2675
|
+
header.append(self._table_data[table_id][mapped_row])
|
|
2676
|
+
# assign this row to the deepest open group, or root
|
|
2677
|
+
elif stack:
|
|
2678
|
+
nodes[stack[-1]]["rows"].append(self._table_data[table_id][mapped_row])
|
|
2679
|
+
else:
|
|
2680
|
+
root_rows.append(self._table_data[table_id][mapped_row])
|
|
2681
|
+
|
|
2682
|
+
row_mapper[row] = mapped_row
|
|
2683
|
+
row += 1
|
|
2684
|
+
|
|
2685
|
+
# helper to convert node dicts to nested mapping (keys -> children or rows)
|
|
2686
|
+
def node_to_structure(node: dict):
|
|
2687
|
+
if not node["children"]:
|
|
2688
|
+
return node["rows"]
|
|
2689
|
+
out = {}
|
|
2690
|
+
for child_key, child_node in node["children"].items():
|
|
2691
|
+
out[child_key] = node_to_structure(child_node)
|
|
2692
|
+
# if this node also has rows in addition to children, include them under a special key
|
|
2693
|
+
if node["rows"]:
|
|
2694
|
+
out["_rows"] = node["rows"]
|
|
2695
|
+
return out
|
|
2696
|
+
|
|
2697
|
+
maximally_nested = {}
|
|
2698
|
+
for key, node in root_children.items():
|
|
2699
|
+
maximally_nested[key] = node_to_structure(node)
|
|
2700
|
+
if root_rows:
|
|
2701
|
+
maximally_nested["_rows"] = root_rows
|
|
2702
|
+
|
|
2703
|
+
self._table_categories_data[table_id] = maximally_nested
|
|
2704
|
+
self._table_categories_row_mapper[table_id] = {
|
|
2705
|
+
row: row_uuid_to_offset[uuid]
|
|
2706
|
+
for row, uuid in enumerate(
|
|
2707
|
+
uuid for uuid in row_uid_for_index if uuid not in group_uuids
|
|
2708
|
+
)
|
|
2709
|
+
}
|
|
2692
2710
|
|
|
2693
2711
|
|
|
2694
2712
|
def rgb(obj) -> RGB:
|