numbers-parser 4.15.1__tar.gz → 4.16.2__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.
Files changed (99) hide show
  1. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/PKG-INFO +27 -25
  2. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/README.md +8 -0
  3. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/pyproject.toml +66 -57
  4. numbers_parser-4.16.2/setup.cfg +4 -0
  5. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/_unpack_numbers.py +1 -7
  6. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/constants.py +11 -2
  7. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/document.py +28 -8
  8. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/model.py +122 -2
  9. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/numbers_cache.py +1 -1
  10. numbers_parser-4.16.2/src/numbers_parser.egg-info/PKG-INFO +494 -0
  11. numbers_parser-4.16.2/src/numbers_parser.egg-info/SOURCES.txt +97 -0
  12. numbers_parser-4.16.2/src/numbers_parser.egg-info/dependency_links.txt +1 -0
  13. numbers_parser-4.16.2/src/numbers_parser.egg-info/entry_points.txt +4 -0
  14. numbers_parser-4.16.2/src/numbers_parser.egg-info/requires.txt +8 -0
  15. numbers_parser-4.16.2/src/numbers_parser.egg-info/top_level.txt +1 -0
  16. numbers_parser-4.16.2/tests/test_all_formulas.py +88 -0
  17. numbers_parser-4.16.2/tests/test_api_change.py +28 -0
  18. numbers_parser-4.16.2/tests/test_borders.py +331 -0
  19. numbers_parser-4.16.2/tests/test_bullets.py +51 -0
  20. numbers_parser-4.16.2/tests/test_cat_numbers.py +283 -0
  21. numbers_parser-4.16.2/tests/test_categories.py +687 -0
  22. numbers_parser-4.16.2/tests/test_coverage.py +307 -0
  23. numbers_parser-4.16.2/tests/test_create_cells.py +82 -0
  24. numbers_parser-4.16.2/tests/test_csv2numbers.py +337 -0
  25. numbers_parser-4.16.2/tests/test_currency.py +25 -0
  26. numbers_parser-4.16.2/tests/test_folder.py +16 -0
  27. numbers_parser-4.16.2/tests/test_formatting.py +955 -0
  28. numbers_parser-4.16.2/tests/test_formulas.py +404 -0
  29. numbers_parser-4.16.2/tests/test_issues.py +608 -0
  30. numbers_parser-4.16.2/tests/test_large.py +39 -0
  31. numbers_parser-4.16.2/tests/test_memory_leaks.py +28 -0
  32. numbers_parser-4.16.2/tests/test_merges.py +79 -0
  33. numbers_parser-4.16.2/tests/test_package.py +58 -0
  34. numbers_parser-4.16.2/tests/test_properties.py +56 -0
  35. numbers_parser-4.16.2/tests/test_roman.py +24 -0
  36. numbers_parser-4.16.2/tests/test_save.py +348 -0
  37. numbers_parser-4.16.2/tests/test_slices.py +144 -0
  38. numbers_parser-4.16.2/tests/test_styles.py +444 -0
  39. numbers_parser-4.16.2/tests/test_table_size.py +115 -0
  40. numbers_parser-4.16.2/tests/test_tables.py +203 -0
  41. numbers_parser-4.16.2/tests/test_unpack_numbers.py +206 -0
  42. numbers_parser-4.16.2/tests/test_unsupported.py +8 -0
  43. numbers_parser-4.16.2/tests/test_uuids.py +37 -0
  44. numbers_parser-4.16.2/tests/test_version.py +52 -0
  45. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/LICENSE.rst +0 -0
  46. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/__init__.py +0 -0
  47. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/_cat_numbers.py +0 -0
  48. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/_csv2numbers.py +0 -0
  49. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/bullets.py +0 -0
  50. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/cell.py +0 -0
  51. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/containers.py +0 -0
  52. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/currencies.py +0 -0
  53. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/data/empty.numbers +0 -0
  54. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/exceptions.py +0 -0
  55. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/experimental.py +0 -0
  56. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/formula.py +0 -0
  57. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TNArchives_pb2.py +0 -0
  58. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TNArchives_sos_pb2.py +0 -0
  59. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TNCommandArchives_pb2.py +0 -0
  60. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TNCommandArchives_sos_pb2.py +0 -0
  61. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSAArchives_pb2.py +0 -0
  62. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSAArchives_sos_pb2.py +0 -0
  63. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSACommandArchives_sos_pb2.py +0 -0
  64. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSCEArchives_pb2.py +0 -0
  65. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSCH3DArchives_pb2.py +0 -0
  66. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSCHArchives_Common_pb2.py +0 -0
  67. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSCHArchives_GEN_pb2.py +0 -0
  68. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSCHArchives_pb2.py +0 -0
  69. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSCHArchives_sos_pb2.py +0 -0
  70. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSCHCommandArchives_pb2.py +0 -0
  71. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSCHPreUFFArchives_pb2.py +0 -0
  72. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSCKArchives_pb2.py +0 -0
  73. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSCKArchives_sos_pb2.py +0 -0
  74. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSDArchives_pb2.py +0 -0
  75. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSDArchives_sos_pb2.py +0 -0
  76. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSDCommandArchives_pb2.py +0 -0
  77. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSKArchives_pb2.py +0 -0
  78. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSPArchiveMessages_pb2.py +0 -0
  79. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSPDatabaseMessages_pb2.py +0 -0
  80. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSPMessages_pb2.py +0 -0
  81. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSSArchives_pb2.py +0 -0
  82. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSSArchives_sos_pb2.py +0 -0
  83. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSTArchives_pb2.py +0 -0
  84. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSTArchives_sos_pb2.py +0 -0
  85. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSTCommandArchives_pb2.py +0 -0
  86. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSTStylePropertyArchiving_pb2.py +0 -0
  87. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSWPArchives_pb2.py +0 -0
  88. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSWPArchives_sos_pb2.py +0 -0
  89. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/TSWPCommandArchives_pb2.py +0 -0
  90. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/__init__.py +0 -0
  91. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/fontmap.py +0 -0
  92. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/functionmap.py +0 -0
  93. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/generated/mapping.py +0 -0
  94. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/iwafile.py +0 -0
  95. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/iwork.py +0 -0
  96. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/numbers_uuid.py +0 -0
  97. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/roman.py +0 -0
  98. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/tokenizer.py +0 -0
  99. {numbers_parser-4.15.1 → numbers_parser-4.16.2}/src/numbers_parser/xrefs.py +0 -0
@@ -1,31 +1,26 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: numbers-parser
3
- Version: 4.15.1
3
+ Version: 4.16.2
4
4
  Summary: Read and write Apple Numbers spreadsheets
5
- License: MIT
6
- Author: Jon Connell
7
- Author-email: python@figsandfudge.com
8
- Requires-Python: >=3.9,<4.0
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.9
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Classifier: Programming Language :: Python :: 3.13
5
+ Author-email: Jon Connell <python@figsandfudge.com>
6
+ License-Expression: MIT
7
+ Project-URL: repository, https://github.com/masaccio/numbers-parser
8
+ Project-URL: documentation, https://github.com/masaccio/numbers-parser/blob/main/README.md
17
9
  Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
18
- Requires-Dist: compact-json (>=1.1.3,<2.0.0)
19
- Requires-Dist: enum-tools (>=0.11)
20
- Requires-Dist: importlib-resources (>=6.1)
21
- Requires-Dist: protobuf (>=4.0,<6.0)
22
- Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
23
- Requires-Dist: python-snappy (>=0.7,<0.8)
24
- Requires-Dist: setuptools (>=70.0.0)
25
- Requires-Dist: sigfig (>=1.3.3,<2.0.0)
26
- Project-URL: Documentation, https://github.com/masaccio/numbers-parser/blob/main/README.md
27
- Project-URL: Repository, https://github.com/masaccio/numbers-parser
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: <4.0,>=3.9
28
13
  Description-Content-Type: text/markdown
14
+ License-File: LICENSE.rst
15
+ Requires-Dist: compact-json<2.0.0,>=1.1.3
16
+ Requires-Dist: protobuf<6.0,>=4.0
17
+ Requires-Dist: python-snappy<1.0,>=0.7
18
+ Requires-Dist: sigfig<2.0.0,>=1.3.3
19
+ Requires-Dist: setuptools>=70.0.0
20
+ Requires-Dist: importlib-resources>=6.1
21
+ Requires-Dist: enum-tools>=0.11
22
+ Requires-Dist: python-dateutil<3.0.0.0,>=2.9.0.post0
23
+ Dynamic: license-file
29
24
 
30
25
  # numbers-parser
31
26
 
@@ -485,8 +480,15 @@ The following limitations are expected to always remain:
485
480
  - Password-encrypted documents cannot be opened. You must first re-save without
486
481
  a password to read (see [issue 88](https://github.com/masaccio/numbers-parser/issues/88) for details).
487
482
  A UnsupportedError exception is raised when such documents are opened.
483
+ - Due to changes in the format of Numbers documents, decoding of category groups
484
+ (introduced in `numbers-parser` version 4.16) is supported only for documents
485
+ created by Numbers 12.0 and later. No warnings are issued for earlier
486
+ Numbers documents.
487
+ - Only standard macOS fonts are not supported. If a document includes a non-standard
488
+ font, numbers-parser will issue a UnsupportedWarning and default styles to
489
+ Helvetica Neue. Reading font names from the system would add additional system-specific
490
+ dependencies to the package and so this is not planned to changed.
488
491
 
489
492
  ## License
490
493
 
491
494
  All code in this repository is licensed under the [MIT License](https://github.com/masaccio/numbers-parser/blob/master/LICENSE.rst).
492
-
@@ -456,6 +456,14 @@ The following limitations are expected to always remain:
456
456
  - Password-encrypted documents cannot be opened. You must first re-save without
457
457
  a password to read (see [issue 88](https://github.com/masaccio/numbers-parser/issues/88) for details).
458
458
  A UnsupportedError exception is raised when such documents are opened.
459
+ - Due to changes in the format of Numbers documents, decoding of category groups
460
+ (introduced in `numbers-parser` version 4.16) is supported only for documents
461
+ created by Numbers 12.0 and later. No warnings are issued for earlier
462
+ Numbers documents.
463
+ - Only standard macOS fonts are not supported. If a document includes a non-standard
464
+ font, numbers-parser will issue a UnsupportedWarning and default styles to
465
+ Helvetica Neue. Reading font names from the system would add additional system-specific
466
+ dependencies to the package and so this is not planned to changed.
459
467
 
460
468
  ## License
461
469
 
@@ -1,73 +1,81 @@
1
- [tool.poetry]
2
- authors = ["Jon Connell <python@figsandfudge.com>"]
1
+ [project]
2
+ authors = [
3
+ {name = "Jon Connell", email = "python@figsandfudge.com"},
4
+ ]
5
+ license = "MIT"
6
+ requires-python = "<4.0,>=3.9"
7
+ dependencies = [
8
+ "compact-json<2.0.0,>=1.1.3",
9
+ "protobuf<6.0,>=4.0",
10
+ "python-snappy<1.0,>=0.7",
11
+ "sigfig<2.0.0,>=1.3.3",
12
+ "setuptools>=70.0.0",
13
+ "importlib-resources>=6.1",
14
+ "enum-tools>=0.11",
15
+ "python-dateutil<3.0.0.0,>=2.9.0.post0",
16
+ ]
3
17
  classifiers = [
4
- "Topic :: Office/Business :: Financial :: Spreadsheet",
5
- "Programming Language :: Python :: 3",
6
- "Operating System :: OS Independent",
18
+ "Topic :: Office/Business :: Financial :: Spreadsheet",
19
+ "Programming Language :: Python :: 3",
20
+ "Operating System :: OS Independent",
7
21
  ]
8
22
  description = "Read and write Apple Numbers spreadsheets"
9
- documentation = "https://github.com/masaccio/numbers-parser/blob/main/README.md"
10
- license = "MIT"
11
23
  name = "numbers-parser"
12
- packages = [{include = "numbers_parser", from = "src"}]
13
24
  readme = "README.md"
25
+ version = "4.16.2"
26
+
27
+ [project.urls]
14
28
  repository = "https://github.com/masaccio/numbers-parser"
15
- version = "4.15.1"
29
+ documentation = "https://github.com/masaccio/numbers-parser/blob/main/README.md"
16
30
 
17
- [tool.poetry.scripts]
31
+ [project.scripts]
18
32
  cat-numbers = "numbers_parser._cat_numbers:main"
19
33
  unpack-numbers = "numbers_parser._unpack_numbers:main"
20
34
  csv2numbers = "numbers_parser._csv2numbers:main"
21
35
 
22
- [tool.poetry.dependencies]
23
- compact-json = "^1.1.3"
24
- protobuf = ">=4.0,<6.0"
25
- python = ">=3.9,<4.0"
26
- python-snappy = "^0.7"
27
- sigfig = "^1.3.3"
28
- setuptools = ">=70.0.0"
29
- importlib-resources = ">=6.1"
30
- enum-tools = ">=0.11"
31
- python-dateutil = "^2.9.0.post0"
32
-
33
- [tool.poetry.group.dev.dependencies]
34
- gprof2dot = "^2022.7.29"
35
- line-profiler = "^4.0.3"
36
- mock = ">=5.1.0"
37
- pytest = ">=7.2.0"
38
- pytest-check = ">=1.0"
39
- pytest-console-scripts = "^1.3.1"
40
- pytest-cov = ">=4.0,>=5.0"
41
- pytest-xdist = "^3.3.1"
42
- ruff = "*"
43
- tox = "^4.11.4"
44
- python-magic = ">=0.4"
45
- tqdm = ">=4.66"
46
- colorama = "^0.4.6"
47
- pympler = "^1.1"
48
-
49
- [tool.poetry.group.docs]
50
- optional = true
51
-
52
- [tool.poetry.group.docs.dependencies]
53
- sphinx = ">= 7.3"
54
- enum-tools = ">=0.11"
55
- sphinx-toolbox = ">=3.5"
56
- sphinx-nefertiti = ">=0.7"
57
- sphinx-markdown-builder = ">=0.6"
58
- sphinx-copybutton = ">=0.5"
59
-
60
- [tool.poetry.group.bootstrap]
61
- optional = true
62
-
63
- [tool.poetry.group.bootstrap.dependencies]
64
- pyobjc-core = ">=10.2"
65
- pyobjc-framework-Cocoa = ">=10.2"
66
- py2app = ">=0.28"
36
+ [dependency-groups]
37
+ dev = [
38
+ "gprof2dot<2023.0.0,>=2022.7.29",
39
+ "line-profiler<5.0.0,>=4.0.3",
40
+ "mock>=5.1.0",
41
+ "pytest>=7.2.0",
42
+ "pytest-check>=1.0",
43
+ "pytest-console-scripts<2.0.0,>=1.3.1",
44
+ "pytest-cov>=4.0,>=5.0",
45
+ "pytest-xdist<4.0.0,>=3.3.1",
46
+ "ruff",
47
+ "tox<5.0.0,>=4.11.4",
48
+ "python-magic>=0.4",
49
+ "tqdm>=4.66",
50
+ "colorama<1.0.0,>=0.4.6",
51
+ "pympler<2.0,>=1.1",
52
+ ]
53
+ docs = [
54
+ "sphinx>=7.3",
55
+ "enum-tools>=0.11",
56
+ "sphinx-toolbox>=3.5",
57
+ "sphinx-nefertiti>=0.7",
58
+ "sphinx-markdown-builder>=0.6",
59
+ "sphinx-copybutton>=0.5",
60
+ ]
61
+ bootstrap = [
62
+ "pyobjc-core>=10.2",
63
+ "pyobjc-framework-Cocoa>=10.2",
64
+ "py2app>=0.28",
65
+ ]
67
66
 
68
67
  [build-system]
69
- build-backend = "poetry.core.masonry.api"
70
- requires = ["poetry-core>=1.0.0"]
68
+ requires = ["setuptools >= 61.0"]
69
+ build-backend = "setuptools.build_meta"
70
+
71
+ [tool.setuptools.packages.find]
72
+ where = ["src"]
73
+ include = ["numbers_parser*"]
74
+ exclude = ["*tests*"]
75
+ namespaces = false # disable scanning PEP 420 namespaces
76
+
77
+ [tool.setuptools.package-data]
78
+ "numbers_parser" = ["data/*"]
71
79
 
72
80
  [tool.coverage.run]
73
81
  branch = true
@@ -189,3 +197,4 @@ ban-relative-imports = "all"
189
197
  "src/build/protodump.py" = ["PLR2004", "INP001", "PTH", "S110", "N806"]
190
198
  "src/debug/**" = ["INP001"]
191
199
  "tests/**" = ["PLR2004", "S101", "D103", "ANN201", "ANN001"]
200
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -49,13 +49,7 @@ class NumbersUnpacker(IWorkHandler):
49
49
  if self.compact_json or self.pretty:
50
50
  formatter = Formatter()
51
51
  formatter.indent_spaces = 2
52
- formatter.max_inline_complexity = 100
53
- formatter.max_compact_list_complexity = 100
54
- formatter.max_inline_length = 160
55
- formatter.max_compact_list_complexity = 2
56
- formatter.simple_bracket_padding = True
57
- formatter.nested_bracket_padding = False
58
- formatter.always_expand_depth = 10
52
+ formatter.max_inline_length = 180
59
53
  pretty_json = formatter.serialize(data)
60
54
  out.write(pretty_json)
61
55
  else:
@@ -5,15 +5,16 @@ from math import ceil
5
5
 
6
6
  import enum_tools.documentation
7
7
 
8
+ # Path to package date varies by Python version
8
9
  try:
9
10
  from importlib.resources import files
10
- # Can't cover exception using modern python
11
11
  except ImportError: # pragma: nocover
12
12
  from importlib_resources import files
13
13
 
14
14
  __all__ = [
15
15
  "CellPadding",
16
16
  "CellType",
17
+ "CellValueType",
17
18
  "ControlFormattingType",
18
19
  "DurationStyle",
19
20
  "DurationUnits",
@@ -112,7 +113,7 @@ def _day_of_year(value: datetime) -> int:
112
113
 
113
114
  def _week_of_month(value: datetime) -> int:
114
115
  """Return the week number in a month for a datetime."""
115
- return int(ceil((value.day + value.replace(day=1).weekday()) / 7.0))
116
+ return ceil((value.day + value.replace(day=1).weekday()) / 7.0)
116
117
 
117
118
 
118
119
  DATETIME_FIELD_MAP = OrderedDict(
@@ -399,6 +400,14 @@ class CellInteractionType(IntEnum):
399
400
  TOGGLE = 8
400
401
 
401
402
 
403
+ class CellValueType(IntEnum):
404
+ NIL_TYPE = 1
405
+ BOOLEAN_TYPE = 2
406
+ DATE_TYPE = 3
407
+ NUMBER_TYPE = 4
408
+ STRING_TYPE = 5
409
+
410
+
402
411
  CONTROL_CELL_TYPE_MAP = {
403
412
  FormattingType.POPUP: CellInteractionType.POPUP,
404
413
  FormattingType.SLIDER: CellInteractionType.SLIDER,
@@ -40,14 +40,6 @@ if TYPE_CHECKING: # pragma: nocover
40
40
  __all__ = ["Document", "Sheet", "Table"]
41
41
 
42
42
 
43
- # class Sheet:
44
- # pass
45
-
46
-
47
- # class Table:
48
- # pass
49
-
50
-
51
43
  class Document:
52
44
  """
53
45
  Create an instance of a new Numbers document.
@@ -1007,6 +999,34 @@ class Table(Cacheable):
1007
999
  msg = "style must be a Style object or style name"
1008
1000
  raise TypeError(msg)
1009
1001
 
1002
+ def categorized_data(self) -> dict | None:
1003
+ """
1004
+ Return the table's data organised into categories, if enabled or ``None``
1005
+ if the table has not had categoried enabled.
1006
+
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.
1010
+
1011
+ Example
1012
+ -------
1013
+ .. code:: python
1014
+
1015
+ "Transport": [
1016
+ {"Description": "Airplane", "Category": "Transport" },
1017
+ {"Description": "Bicycle", "Category": "Transport" },
1018
+ {"Description": "Bus", "Category": "Transport"},
1019
+ ],
1020
+ "Fruit": [
1021
+ {"Description": "Apple", "Category": "Fruit" },
1022
+ {"Description": "Banana", "Category": "Fruit" },
1023
+ ],
1024
+
1025
+ For tables with multiple categories, the top-level dictionary is nested.
1026
+
1027
+ """
1028
+ return self._model.table_category_data(self._table_id)
1029
+
1010
1030
  def add_row(
1011
1031
  self,
1012
1032
  num_rows: int | None = 1,
@@ -8,6 +8,8 @@ from itertools import chain
8
8
  from math import floor
9
9
  from pathlib import Path
10
10
  from struct import pack
11
+ from typing import TYPE_CHECKING
12
+ from warnings import warn
11
13
 
12
14
  from numbers_parser.bullets import (
13
15
  BULLET_CONVERSION,
@@ -37,6 +39,7 @@ from numbers_parser.constants import (
37
39
  CUSTOM_TEXT_PLACEHOLDER,
38
40
  DEFAULT_COLUMN_WIDTH,
39
41
  DEFAULT_DOCUMENT,
42
+ DEFAULT_FONT,
40
43
  DEFAULT_PRE_BNC_BYTES,
41
44
  DEFAULT_ROW_HEIGHT,
42
45
  DEFAULT_TABLE_OFFSET,
@@ -48,11 +51,12 @@ from numbers_parser.constants import (
48
51
  MAX_TILE_SIZE,
49
52
  PACKAGE_ID,
50
53
  CellInteractionType,
54
+ CellValueType,
51
55
  FormatType,
52
56
  OwnerKind,
53
57
  )
54
58
  from numbers_parser.containers import ObjectStore
55
- from numbers_parser.exceptions import UnsupportedError
59
+ from numbers_parser.exceptions import UnsupportedError, UnsupportedWarning
56
60
  from numbers_parser.formula import TableFormulas
57
61
  from numbers_parser.generated import TNArchives_pb2 as TNArchives
58
62
  from numbers_parser.generated import TSAArchives_pb2 as TSAArchives
@@ -76,6 +80,9 @@ from numbers_parser.numbers_cache import Cacheable, cache
76
80
  from numbers_parser.numbers_uuid import NumbersUUID, uuid_to_hex
77
81
  from numbers_parser.xrefs import CellRange, ScopedNameRefCache
78
82
 
83
+ if TYPE_CHECKING:
84
+ from datetime import datetime
85
+
79
86
 
80
87
  def create_font_name_map(font_map: dict) -> dict:
81
88
  new_font_map = {}
@@ -238,6 +245,7 @@ class _NumbersModel(Cacheable):
238
245
  "left": defaultdict(),
239
246
  }
240
247
  self.name_ref_cache = ScopedNameRefCache(self)
248
+ self.missing_fonts = {}
241
249
  self.calculate_table_uuid_map()
242
250
 
243
251
  def save(self, filepath: Path, package: bool) -> None:
@@ -289,7 +297,7 @@ class _NumbersModel(Cacheable):
289
297
  # which is an ordered list that matches the storage buffers, but
290
298
  # identifies which row a storage buffer belongs to (empty rows have
291
299
  # no storage buffers).
292
- row_bucket_map = {i: None for i in range(self.objects[table_id].number_of_rows)}
300
+ row_bucket_map = dict.fromkeys(range(self.objects[table_id].number_of_rows))
293
301
  bds = self.objects[table_id].base_data_store
294
302
  bucket_ids = [x.identifier for x in bds.rowHeaders.buckets]
295
303
  idx = 0
@@ -2268,6 +2276,16 @@ class _NumbersModel(Cacheable):
2268
2276
  def cell_font_name(self, obj: Cell | object) -> str:
2269
2277
  style = self.cell_text_style(obj) if isinstance(obj, Cell) else obj
2270
2278
  font_name = self.char_property(style, "font_name")
2279
+ if font_name not in FONT_NAME_TO_FAMILY:
2280
+ if font_name not in self.missing_fonts:
2281
+ warn(
2282
+ f"Custom font '{font_name}' unsupported; falling back to {DEFAULT_FONT}",
2283
+ UnsupportedWarning,
2284
+ stacklevel=2,
2285
+ )
2286
+ self.missing_fonts[font_name] = True
2287
+ return DEFAULT_FONT
2288
+
2271
2289
  return FONT_NAME_TO_FAMILY[font_name]
2272
2290
 
2273
2291
  def cell_first_indent(self, obj: Cell | object) -> float:
@@ -2550,6 +2568,108 @@ class _NumbersModel(Cacheable):
2550
2568
  # datas never appears to be an empty list (default themes include images)
2551
2569
  return max(image_ids) + 1
2552
2570
 
2571
+ def table_category_data(self, table_id: int) -> dict | None:
2572
+ category_owner_id = self.objects[table_id].category_owner.identifier
2573
+ category_archive_id = self.objects[category_owner_id].group_by[0].identifier
2574
+ category_archive = self.objects[category_archive_id]
2575
+ if not category_archive.is_enabled:
2576
+ return None
2577
+
2578
+ table_info = self.objects[self.table_info_id(table_id)]
2579
+ category_order = self.objects[table_info.category_order.identifier]
2580
+ row_uid_map = self.objects[category_order.uid_map.identifier]
2581
+ sorted_row_uuids = [
2582
+ NumbersUUID(row_uid_map.sorted_row_uids[i]).hex for i in row_uid_map.row_uid_for_index
2583
+ ]
2584
+
2585
+ data = self._table_data[table_id]
2586
+ header = [cell.value for cell in data[0]]
2587
+
2588
+ def index_set_to_offsets(index_set: TSCEArchives.IndexSetArchive) -> list[int]:
2589
+ """Convert an IndexSetArchive to a list of offsets."""
2590
+ offsets = []
2591
+ for entry in index_set.entries:
2592
+ if entry.HasField("range_end"):
2593
+ offsets += list(range(entry.range_begin, entry.range_end + 1))
2594
+ else:
2595
+ offsets += list(range(entry.range_begin, entry.range_begin + 1))
2596
+ return offsets
2597
+
2598
+ def cell_value_to_key(
2599
+ cell_value: TSCEArchives.CellValueArchive,
2600
+ ) -> str | int | bool | datetime:
2601
+ """Convert a CellValueArchive to a key."""
2602
+ cell_value_type = cell_value.cell_value_type
2603
+ if cell_value_type == CellValueType.STRING_TYPE:
2604
+ return cell_value.string_value.value
2605
+ if cell_value_type == CellValueType.NUMBER_TYPE:
2606
+ return cell_value.number_value.value
2607
+ if cell_value_type == CellValueType.BOOLEAN_TYPE:
2608
+ return cell_value.boolean_value.value
2609
+ # Must be DATE_TYPE
2610
+ return cell_value.date_value.value
2611
+
2612
+ group_node_to_key = {
2613
+ NumbersUUID(self.objects[_id].group_uid).hex: cell_value_to_key(
2614
+ self.objects[_id].group_cell_value,
2615
+ )
2616
+ for _id in self.find_refs("GroupNodeArchive")
2617
+ }
2618
+ group_uuids = [NumbersUUID(x.group_uid).hex for x in category_archive.group_node_root.child]
2619
+ group_uuids = [uuid for uuid in sorted_row_uuids if uuid in group_uuids]
2620
+
2621
+ def group_hierarchy(parent: str, children: list):
2622
+ nodes = {}
2623
+ for child in children:
2624
+ group_uuid = NumbersUUID(child.group_uid).hex
2625
+ if len(child.child) > 0:
2626
+ nodes[group_uuid] = group_hierarchy(group_uuid, child.child)
2627
+ else:
2628
+ nodes[group_uuid] = None
2629
+ return nodes
2630
+
2631
+ def assign_rows_to_categories(parent: str, children: list, categories: dict):
2632
+ for child in children:
2633
+ group_uuid = NumbersUUID(child.group_uid).hex
2634
+ if len(child.child) == 0:
2635
+ key = cell_value_to_key(child.group_cell_value)
2636
+
2637
+ row_offsets = index_set_to_offsets(child.row_lookup_uids)
2638
+ categories[group_uuid] = {
2639
+ "key": key,
2640
+ "parent": parent,
2641
+ "rows": [
2642
+ {header[col]: cell.value for col, cell in enumerate(data[row])}
2643
+ for row in row_offsets
2644
+ ],
2645
+ }
2646
+ else:
2647
+ categories[group_uuid] = {
2648
+ "key": group_node_to_key[group_uuid],
2649
+ "parent": parent,
2650
+ "rows": None,
2651
+ }
2652
+ assign_rows_to_categories(group_uuid, child.child, categories)
2653
+
2654
+ category_tree = group_hierarchy(
2655
+ NumbersUUID(category_archive.group_node_root.group_uid).hex,
2656
+ category_archive.group_node_root.child,
2657
+ )
2658
+
2659
+ categories = {}
2660
+ assign_rows_to_categories(None, category_archive.group_node_root.child, categories)
2661
+
2662
+ def merge_trees(a: dict, b: dict):
2663
+ new_tree = {}
2664
+ for k, v in a.items():
2665
+ if v is not None:
2666
+ new_tree[b[k]["key"]] = merge_trees(v, b)
2667
+ else:
2668
+ new_tree[b[k]["key"]] = b[k]["rows"]
2669
+ return new_tree
2670
+
2671
+ return merge_trees(category_tree, categories)
2672
+
2553
2673
 
2554
2674
  def rgb(obj) -> RGB:
2555
2675
  """Convert a TSPArchives.Color into an RGB tuple."""
@@ -3,7 +3,7 @@ from functools import wraps
3
3
 
4
4
 
5
5
  class Cacheable:
6
- def __new__(cls, *args, **kwargs):
6
+ def __new__(cls, *_args, **_kwargs):
7
7
  obj = object.__new__(cls)
8
8
  obj._cache = defaultdict(lambda: defaultdict(dict))
9
9
  return obj